-
-
Notifications
You must be signed in to change notification settings - Fork 8.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[JENKINS-38867] Optimize Actionable.getAllActions #2582
Changes from 2 commits
f888361
d26c742
3679ee9
ae1ed2c
1ba0ef8
4d2bc22
8a99648
69ff255
b514321
76a1b75
dfdcdc8
5c2c70b
b83b7aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,7 +23,11 @@ | |
*/ | ||
package hudson.model; | ||
|
||
import com.google.common.cache.CacheBuilder; | ||
import com.google.common.cache.CacheLoader; | ||
import com.google.common.cache.LoadingCache; | ||
import hudson.ExtensionList; | ||
import hudson.ExtensionListListener; | ||
import hudson.Util; | ||
import java.util.ArrayList; | ||
import java.util.Collection; | ||
|
@@ -91,18 +95,51 @@ public List<Action> getActions() { | |
*/ | ||
@Exported(name="actions") | ||
public final List<? extends Action> getAllActions() { | ||
List<Action> _actions = new ArrayList<Action>(getActions()); | ||
for (TransientActionFactory<?> taf : ExtensionList.lookup(TransientActionFactory.class)) { | ||
if (taf.type().isInstance(this)) { | ||
try { | ||
_actions.addAll(createFor(taf)); | ||
} catch (Exception e) { | ||
LOGGER.log(Level.SEVERE, "Could not load actions from " + taf + " for " + this, e); | ||
List<Action> _actions = getActions(); | ||
boolean adding = false; | ||
synchronized (Actionable.class) { | ||
if (factoryCache == null) { | ||
@SuppressWarnings("rawtypes") | ||
final ExtensionList<TransientActionFactory> allFactories = ExtensionList.lookup(TransientActionFactory.class); | ||
factoryCache = CacheBuilder.newBuilder().build(new CacheLoader<Class<? extends Actionable>, Collection<? extends TransientActionFactory<?>>>() { | ||
@Override | ||
public Collection<? extends TransientActionFactory<?>> load(Class<? extends Actionable> implType) throws Exception { | ||
List<TransientActionFactory<?>> factories = new ArrayList<>(); | ||
for (TransientActionFactory<?> taf : allFactories) { | ||
if (taf.type().isAssignableFrom(implType)) { | ||
factories.add(taf); | ||
} | ||
} | ||
return factories; | ||
} | ||
}); | ||
allFactories.addListener(new ExtensionListListener() { | ||
@Override | ||
public void onChange() { | ||
factoryCache.invalidateAll(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How often will this get called? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After startup it should only get called if you dynamically install a plugin, which is rare. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair. I can see other places this strategy might be useful, so may borrow it in the future. |
||
} | ||
}); | ||
} | ||
} | ||
for (TransientActionFactory<?> taf : factoryCache.getUnchecked(getClass())) { | ||
Collection<? extends Action> additions; | ||
try { | ||
additions = createFor(taf); | ||
} catch (Exception e) { | ||
LOGGER.log(Level.SEVERE, "Could not load actions from " + taf + " for " + this, e); | ||
continue; | ||
} | ||
if (!additions.isEmpty()) { | ||
if (!adding) { // need to make a copy | ||
adding = true; | ||
_actions = new ArrayList<>(_actions); | ||
} | ||
_actions.addAll(additions); | ||
} | ||
} | ||
return Collections.unmodifiableList(_actions); | ||
} | ||
private static LoadingCache<Class<? extends Actionable>, Collection<? extends TransientActionFactory<?>>> factoryCache; | ||
private <T> Collection<? extends Action> createFor(TransientActionFactory<T> taf) { | ||
return taf.createFor(taf.type().cast(this)); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🐛 ❗️ 💥 We're synchronizing every single getAllActions call on this single class. Instant lock contention all over the place.
Synchronize on this.getClass() I think. If we can avoid synchronization at all we should, though.
Edit: unless my coffee is still kicking in and I've misunderstood -- intent is to synchronize on this specific class, not the overall actionable if at all possible though, and better neither.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually @svanoort you are reading this wrong.
factoryClass
is a lazy singleton cache. Would be better to leverage the lazy instantiation pattern...And then here we just go
ResourceHolder.factoryCache
without care and the JVM can optimize better.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, so more coffee it is then. Agree totally that the ResourceHolder approach is far better and will improve performance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I doubt that it matters, since after the cache is initialized during startup the code will just be doing a null check and the JVM is good at optimizing away contention on monitors in simple cases, but if it makes you happier I can switch to a resource holder pattern.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually that is silly. Any kind of cache lookup needs to acquire a lock anyway, so we might as well use just one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
However, there is a more subtle issue with restarting Jenkins which I will work to fix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe, maybe not - explicit contention for something that will be invoked often by numerous threads raises a flag for me on general principles. I agree it shouldn't be as expensive as initially I thought when skimming through (and hopefully won't be a problem), but still worth a 🐜 on general principles.
IIRC most of the hashmaps and concurrent maps are based on locking per-slot and not on the whole, so contention is extremely rare. Actions are likely to get requested by many threads, and frequently, so contention will be high, even if very brief.