Alex headshot

AlBlue’s Blog

Macs, Modularity and More

Eclipse Performance revisited

2015, eclipse, java

With the release of Mars.1 and Neon.2 today, I thought it would be good to see what effect (if any) the optimisations that I’ve been working on have had. So I took Neon and Mars for a spin, and compared the outputs.

I’ve been timing the startup of an application with the org.eclipse.osgi/debug traces; specifically, the loader (which displays whenever a class is being loaded), and bundleTime (which measures the execution time of the Bundle-Activator). By running the application, then hitting the red stop button when the main window is available, it’s possible to get a measure of how fast the application starts up. I don’t do ‘quit’ from the application (because that would cause more classes to be loaded) so I just terminate the JVM at the right point.

However, to automate it, and to allow others to experiment as well, I invested in some time in creating various launch configurations pushed them to https://github.com/alblue/EclipsePerformanceTests.git so that others could replicate my setup. Essentially, there’s an E4 application with no content, that starts up and then shuts down shortly afterwards. There are some external tools that can process these lists to give numbers that are hidden in the application.

That’s the good news. The bad news is that the optimisations so far haven’t had much of an effect; in fact, start-up of Neon is slightly slower than Mars at the moment. Starting an Eclipse SDK instance (the original SDK from http://download.eclipse.org/eclipse/downloads/ as opposed to the EPP, so I can get measurements without automated reporting) led to a start-up time of Mars of around 6.1s and Neon of 6.3s.

Oops.

In fact, the start-up of the empty project has remained around the same, at 1.8s (after files are in the cache). Strangely enough, if you look at the list of classes loaded there are a few more classes that are loaded (such as o.e.core.internal.content.BasicDescription and o.e.core.internal.content.ContentType) which weren’t there before. On the plus side, the total byte size has dropped slightly (about 4k) and we’re now down to 21 activators, from 30 before. This was counterbalanced by the activators' removal as well as a number of other inner classes; for example, the migration of inner classes to lambdas in Lars' commit was a reduction of the load of separate classes. (Lars, if you want to take another one then JobManager would be a good one, as would E4Application and WBWRenderer … but never mind that now.)

Now the additional .content. changes are suspicious, if only because I have pushed a few changes to that recently. I originally thought that the removal of static references were at fault, but it turns out that the move to declarative services caused the problem.

How can that happen, I hear you ask? Well, it’s a damn good question because it took me a while to work that out as well. And the other question – what to do about it – is also another interesting one as well :)

As a side-note, measuring performance of Eclipse at start-up is a little challenging. Unlike correctness testing (where you can run tests in the IDE) the performance of the application, for performance testing there’s a variation depending on whether you are running the code from a JAR or from a project in the workspace (different class loader resolutions are used; there are different paths which load the content if it’s a File or an InputStream, different mechanisms for accessing resources and the like). You can test some deltas before and after, but to test it for real installing it into a host workspace and restarting is the minimum requirement.

There’s also differences between the builds published by the Eclipse infra and ones that your code does; it has been signed, and often gone through the pack200 processing/unprocessing. So the code that ultimately gets delivered is not quite the same as you can test locally. Other minor differences include the version of the Java runtime and compiler as well as a whole host of other potential issues. It’s not as much of a science as trying to minimise the variations to determine testing.

Anyway; back to DS. The changes to the ContentTypeManager included changing the ExtensionRegistry listener to an instance method, and to use DS to assign it (instead of the prior Activator). Why does this single change cause additional classes to be resolved?

It turns out this is a side-effect of the way DS works. When a related service is set, DS looks up the method from the XML file and then uses getDeclaredMethod() to look it up. In this case, it runs something equivalent to ContentTypeManager.class.getDeclaredMethod("setExtensionRegistry"). This is not far off what it used to do before in the Activator. So why does this do anything different now?

Well, the main reason is the way that the Java reflection is implemented. Although the code calls the single variant getDeclaredMethod("name"), internally this expands to getDeclaredMethods() and then filters them afterwards. As a result, you’re not just getting your method; you’re getting all methods. This means that all classes defined as exceptions, parameters or return results in that class will subsequently be loaded even though they are completely unnecessary. Although they aren’t actually initialized (their static blocks aren’t run) the class objects need to be defined so that they have placeholder types in methods that we don’t even need. This will then recurse to super-interfaces and super-classes (but not their contents) which will result in the additional classes being lodaed.

So we traded off the loading of the single Activator class of 5k for four classes which are 54k in size. Oops. Not a sensible trade-off.

The advantage that DS gives us is that it’s not acquired until it’s first used. This should be a boon because it means that we can defer the cost of loading these more expensive classes until we really need it. And do we need a content type manager for an empty window?

Aargh. It’s another Activator. This time, it’s PlatformActivator calling InternalPlatform which calls contentTracker.open. Unfortunately, a ServiceTracker calling open() will then trigger the initialization of the very service that we’re trying to be lazy in instantiating. Sigh.

As a side note, this is why we need a Suppliers factory. Instead of having all these buggy references to eagerly activated ServiceTrackers, we should be delegating to a single implementation that would Do The Right Thing, including deferring opening the tracker until it’s been accessed for the first time. (It would also help Tom, who would in future be able to replace this implementation with other non-OSGi implementations, such as ServiceLoader or whatever might come out of Jigsaw.)

The alternative of course is to replace it with a bunch of DS components; either of these solutions would work. If we can defer the accessing of the IContentTypeManager service, then none of the classes would be loaded.

Unfortunately, there’s no way of fixing the JDK. The new DS specification permits installing services in fields (though I’m not sure if this is exposed in PDE’s DS implementation yet). This wouldn’t help in this particular instance because the setting of the extension registry needs side-effects, which aren’t visible if only a field is set. In addition, the getField() call will perform the same resolution; and there’s more likely to be more fields which are defined with implementation classes than methods (which should generally be interfaces).

We could split the implementation to reduce the number of declared methods; for example, having a ContentTypeManagerDS subclass that exposes the DS required methods may reduce the number of classes that need to be resolved. Another alternative is to have a delegate which implements the interface and forwards the implementation methods; but in this case, the IContentTypeManager is such a large interface (with several super-interfaces and nested types) that this doesn’t buy much. Or we could just revert the commits in this particular case.

The good news is that this doesn’t particularly affect the SDK; the content type manager is used there, and these classes are loaded. So in the real test – how long it takes to spool up the SDK – either of these implementations are likely to be loaded in any case. It’s only in the startup of a simple E4 application that you’re likely to notice the difference; and this has a potential solution with addressing the PlatformActivator and friends.

Update 2015-10-06: I have been asked to stop submitting micro optimisations to Eclipse.