I've been doing some performance analysis work looking at the start
time associated with the JEE product. I'm posting this note as HTML
with screen captures, so I'm not sure the form in which it will
arrive for all recipients. As such, I'll mark each insertion point
with #<number> in case the images arrive as attachments,
presumably in the order I have them in this note. Please use
https://bugs.eclipse.org/bugs/show_bug.cgi?id=484024 for followup
discussions.
This work was driven from a self-hosted launch in which I could
prototype changes to Equinox and the Platform in the workspace along
with the full JEE product in the target platform. (Yes, I used
Oomph to set that up easily.) I've already opened a number of
Bugzillas with Gerrit contributions for various small performance
improvements based on the measurements I've done. Thanks to the
reviewers for spending time on them!
https://bugs.eclipse.org/bugs/show_bug.cgi?id=482642
https://bugs.eclipse.org/bugs/show_bug.cgi?id=482643
https://bugs.eclipse.org/bugs/show_bug.cgi?id=482666
https://bugs.eclipse.org/bugs/show_bug.cgi?id=482673
https://bugs.eclipse.org/bugs/show_bug.cgi?id=483986
https://bugs.eclipse.org/bugs/show_bug.cgi?id=481705
https://bugs.eclipse.org/bugs/show_bug.cgi?id=482680
https://bugs.eclipse.org/bugs/show_bug.cgi?id=482698
https://bugs.eclipse.org/bugs/show_bug.cgi?id=483989
https://bugs.eclipse.org/bugs/show_bug.cgi?id=483991
One of the premises underlying most efforts to make the UI startup
faster has been to be more lazy. One aspect of that is to avoid
starting bundles, especially bundles that are expensive to start.
If we trace profile the time between showing the workspace prompt
and the time when the IDE is finally displayed, we can measure that
important aspect. Here's what we see happening on the main
(display/UI) thread starting up JEE: #1
An important thing to note is that starting a bundle is not in and
of itself expensive. It's what else that bundle does during startup
that can be expensive. Especially important is what it does on the
main thread because that's the bottleneck for displaying the IDE to
the user. It's also important to note that many bundles inevitably
need to be started, forced by things such as contributing a property
tester, one of the things to help the IDE be more lazy. (Or is
there a way to contribute a tester without a bundle start?)
While bundle startup isn't necessarily expensive, loading many
classes is definitely expensive, again especially if it's on the
main thread. Here we see a partial picture (class initialization
can be expensive too) of the class loading involved on the main
thread: #2
Here's the class initialization cost on the main thread: #3
EMF package initialization loads all interfaces of the model, and
loads all models which it depends, so it's quite expensive,
primarily because of class loading.
An alternative approach one might consider is rather than being more
lazy, is being more eager, as long as that eagerness is done on a
different thread that can work in parallel while the main thread is
busy doing the most important thing, that is, displaying the IDE to
the user.
I've prototyped a very rough design in
org.eclipse.ui.internal.ide.application.IDEApplication.start(IApplicationContext)
that uses a thread pool to start bundles that must necessarily
start. Those changes (details described below) have a rather
dramatic impact on bundle start times on the main thread: #4

Also if we look at class loading, that's also dramatically reduced:
#5

As is the cost of class initialization: #6
If I instrument org.eclipse.ui.internal.Workbench to do wall clock
measurements (goodness knows one must take profile measurements with
a grain of salt), and compare the normal startup times
clinit=1218
init=6
start=4327
earlystart=453
total=5551
with startup that does eager background initialization
clinit=1469
init=4
start=3539
earlystart=2
total=5012
we really do see a 10% improvement. These labels mean the
following:
- clinit - the time between when the user hits OK on the open
workspace dialog and the time when the constructor
org.eclipse.ui.internal.Workbench.Workbench(Display,
WorkbenchAdvisor, MApplication, IEclipseContext) has finished
calling super()
- init - the time between clinit and when when
org.eclipse.ui.internal.Workbench.runUI() is called
- start - the time it takes to create the workbench in order to
display it
- earlystart - the time spent early starting all the bundles
registered with IStartup
- total - the total (note that the UI is already visible and
painted before early start begins and that early start happens
on a background thread).
I could imagine the platform core runtime providing an extension
point for eager early startup that would be used to perform early
eager initialization on multiple background threads. It could
support two different phases of eagerness.
- Initialization that can be performed before the workspace
location is known, i.e., that can happen before
ResourcesPlugin.start is called. Unfortunately very little can
be done at this point because most things depend on the
workspace location being established.
- Initialization that can be performed once the workspace
location is known.
The former is useful for plugins that have no direct or indirect
core.resources dependencies; these can be kicked off while the
workspace prompt is showing. The later is useful for kicking off
initialization as soon as the user has chosen the workspace
location, at which point the UI thread starts the processing to
bring up the IDE.
These are relatively expensive things and can be done while the
workspace prompt is being shown, i.e., ICU4J's expensive class
initialization:
{
Map<String, String> bundles = new
LinkedHashMap<>();
bundles.put("com.ibm.icu",
"com.ibm.icu.text.Collator#getInstance()");
//$NON-NLS-1$//$NON-NLS-2$
queue.put(bundles);
}
{
Map<String, String> bundles = new
LinkedHashMap<>();
bundles.put("com.ibm.icu",
"com.ibm.icu.text.DateFormat#getDateTimeInstance()");
//$NON-NLS-1$//$NON-NLS-2$
queue.put(bundles);
}
The basic idea is that we reflectively call the specified methods or
reflectively access the specified fields. The methods above were
chosen based on the following observation: #7
When offloaded to a background thread, their cost is gone from the
main thread: #8
These are expensive things that can be done in parallel before the
UI needs them:
{
Map<String, String> bundles = new
LinkedHashMap<>();
bundles.put("org.eclipse.core.resources",
"org.eclipse.core.resources.ResourcesPlugin");
//$NON-NLS-1$//$NON-NLS-2$
queue.put(bundles);
}
Thread.yield();
{
Map<String, String> bundles = new
LinkedHashMap<>();
bundles.put("org.eclipse.e4.ui.model.workbench",
//$NON-NLS-1$
"org.eclipse.e4.ui.model.application.impl.ApplicationPackageImpl#eINSTANCE");
//$NON-NLS-1$
bundles.put("org.eclipse.emf.ecore",
"org.eclipse.emf.ecore.EcorePackage#eINSTANCE");
//$NON-NLS-1$//$NON-NLS-2$
bundles.put("org.eclipse.emf.ecore.xmi",
//$NON-NLS-1$
"org.eclipse.emf.ecore.xmi.XMIPlugin$Implementation$Activator");
//$NON-NLS-1$
queue.put(bundles);
}
Thread.yield();
{
Map<String, String> bundles = new
LinkedHashMap<>();
bundles.put("org.eclipse.pde.core",
"org.eclipse.pde.internal.core.PDECore"); //$NON-NLS-1$//$NON-NLS-2$
queue.put(bundles);
}
Thread.yield();
{
Map<String, String> bundles = new
LinkedHashMap<>();
bundles.put("org.eclipse.jdt.core",
"org.eclipse.jdt.core.JavaCore"); //$NON-NLS-1$ //$NON-NLS-2$
queue.put(bundles);
}
Thread.yield();
{
Map<String, String> bundles = new
LinkedHashMap<>();
bundles.put("org.eclipse.mylyn.commons.net",
"org.eclipse.mylyn.commons.net.WebUtil#init()");
//$NON-NLS-1$//$NON-NLS-2$
queue.put(bundles);
}
Thread.yield();
{
Map<String, String> bundles = new
LinkedHashMap<>();
bundles.put("org.eclipse.team.core",
"org.eclipse.team.internal.core.TeamPlugin");
//$NON-NLS-1$//$NON-NLS-2$
queue.put(bundles);
}
The above logic is what results in the improved performance shown in
the fourth screen capture (#4) earlier in this note.
One thing that hinders more eager parallel initialization is that
many UI bundles assume they will be started on the main thread,
i.e., they fail with SWT invalid thread access exceptions if one
attempts to start them on a background thread. E.g., PDE's, JDT's,
and Mylyn Tasks' UI plugins cannot be initialized on a background
thread, (though likely they could offload significant costs to
worker threads).
Another issue is that just starting a bundle doesn't necessarily
involve loading many of its classes, which is generally a good
thing, but it's hard to drive more eager class loading as much as
would be beneficial for this prototyping effort. I did try loading
all classes in each bundle being eagerly loaded, but this is just
way more classes than are actually needed and, in the end, hurts
overall performance.
Based on the above observations, one of the things I am changing in
Oomph is to move logic that previously was kicked off from
org.eclipse.oomph.setup.ui.SetupUIPlugin.Implementation.start(BundleContext)
to be kicked off instead from
org.eclipse.oomph.setup.ui.EarlyStartup.earlyStartup() so it happens
after the IDE is displayed to the user. Also, with the following
simple changes, I could also force much of the class loading to
happen early on the worker thread:
static void performStartup()
{
if (SetupTaskPerformer.REMOTE_DEBUG ||
SynchronizerManager.ENABLED || RecorderManager.INSTANCE != null)
{
// This is only to force class loading on a background thread.
}
I.e., before dispatching work to the main thread:
if (!PropertiesUtil.isProperty(PREF_HEADLESS))
{
final Display display = Display.getDefault();
display.asyncExec(new Runnable()
{
Certainly other UI bundles could benefit from this approach, e.g.,
the Mylyn Task UI bundle, without need for any architectural changes
to Eclipse.
One thing I noticed wondering why PDEPlugin was started so early was
to noticed that it's called from
org.eclipse.ui.internal.Workbench.initializeDefaultServices() which
has this code:
StartupThreading.runWithoutExceptions(new StartupRunnable()
{
@Override
public void runWithException() {
// this currently instantiates all players ... sigh
sourceProviderService.readRegistry();
ISourceProvider[] sp =
sourceProviderService.getSourceProviders();
for (int i = 0; i < sp.length; i++) {
evaluationService.addSourceProvider(sp[i]);
if (!(sp[i] instanceof
ActiveContextSourceProvider)) {
contextService.addSourceProvider(sp[i]);
}
}
}
})
Note the "sigh". And yes indeed, in
org.eclipse.ui.internal.services.WorkbenchServiceRegistry.getSourceProviders()
it creates instances of all the source providers and PDE is one of
those. It provides
org.eclipse.pde.internal.ui.views.imagebrowser.ActiveImageSourceProvider
so the single most expensive bundle to start in JEE must be started
early, on the UI thread.
A general take away is to set a breakpoint in your UI bundle
activators to see when they are called. Often this will be
unavoidable, but what you do when the start method is called should
be more carefully considered. In particular, ask yourself, what do
I really need to do on the UI thread as soon as my bundle starts?
Not so much hopefully. And even if it must be done on the UI
thread, can it be deferred until IStartup.earlyStart() is called?
You can always dispatch a runnable to the display thread when early
start is called, so that the user at least has a pretty IDE to look
at and click on while you make it better in the background. Better
still is to do as much work as possible or all work in your early
start method, or to delegate to a Job for that purpose. Your
goodness can wait, but the user should not.
It would probably be good to do this for
org.eclipse.mylyn.internal.tasks.ui.TasksUiPlugin. It has code like
this:
// XXX reconsider if this is necessary
public static class TasksUiStartup implements IStartup {
public void earlyStartup() {
// ignore
}
}
No, this isn't necessary because you contribute
org.eclipse.mylyn.internal.tasks.ui.util.TaskPropertyTester so you
will be started on the UI thread very early. What could you defer
to earlyStart()? What could you do on the worker thread in
earlyStart() instead of in the display thread?
|