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.

Acquiring OSGi Services in Java 8

2015, eclipse, java, osgi

Whilst performing some code-cleanups on Eclipse, Lars made the observation “This looks unnecessarily hard – why isn’t there a simple API for this?”.

The code in question was acquiring an instance of DebugOptions, which is used liberally throughout Eclipse to determine if an option is present to enable debugging. Actually the use of DebugOptions itself wasn’t that much of an issue (though the getBooleanOption is unable to determine whether or not a boolean value is present or is false – but that’s another bug). The problem is that looking up an OSGi service is more than a single-liner, and as such, is the type of thing that probably causes more pain than it needs.

There’s three ways of getting services in OSGi, in reverse order of ease of use:

  • Acquire the BundleContext (such as via the BundleActivator or through a handler like FrameworkUtil) and then use getService() directly
  • Use a ServiceTracker to keep a cache of the services available for quick return
  • Use Declarative Services to instantiate your component and have the services injected in directly

The problem with using Declarative Services is that you yield not only the acquisition of the service but also the component lifetime. It also precludes the use of static methods or integration with other APIs that expect to manage object creation (or indeed, use object creation with constructors or builder patterns).

The other two use a non-trivial amount of code to get running, all for the sake of satisfying a ‘Please give me an instance’ request.

Fortunately, Java 8 provides a simple way of abstracting this; using the Supplier interface. A supplier is something that, when asked, returns an instance of a particular request. It’s also used in a number of different collections to defer object acquisition until it’s required. This fits in with what we’re trying to do – get an instance of a service. So how might it look in OSGi?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class OSGiTracker<T> implements AutoClosable, Supplier<T> {
  private final ServiceTracker<T,T> serviceTracker;
  private boolean closed = true;
  private OSGiTracker(Class<T> target, Class<?> source) {
    if (target == null) {
      throw new IllegalArgumentException("Target cannot be null");
    }
    if (source == null) {
      throw new IllegalArgumentException("Source cannot be null");
    }
    Bundle bundle = FrameworkUtil.getBundle(source);
    BundleContext context = bundle == null ? null : bundle.getBundleContext();
    if (context == null) {
      throw new IllegalArgumentException(
       "Unable to acquire bundle context for " + source.getCanonicalName());
    }
    this.serviceTracker = new ServiceTracker<T,T>(context,target,null);
  }
  public static <T> OSGiTracker<T> supply(Class<T> target, Class<?> source) {
    return new OSGiTracker<>(target, source);
  }
  @Override
  public T get() {
    if(closed) {
      serviceTracker.open();
      closed = false;
    }
    return serviceTracker.getService();
  }
  protected void finalize() throws Throwable {
    close();
    super.finalize();
  }
  public void close() throws Exception {
    if(serviceTracker != null && !closed) {
      serviceTracker.close();
    }
  }
}

This provides a Supplier for a given service, and the only two parameters that are required are the generic target required (e.g. DebugOptions) and the calling class (so that the BundleContext can be resolved). The API doesn’t get much simpler than that. This is how it looks when using it:

1
2
3
4
private final Supplier<DebugOptions> options =
 OSGiTracker.supply(DebugOptions.class, getClass());

private final boolean DEBUG = options.get().getBooleanOption(...)

No worrying about dependencies other than the OSGiTracker class, and the client side API is trivial.

However, in most cases there’s no need to keep a ServiceTracker hanging on since you are using it in a one-shot basis. If the service isn’t there, you want to use a default without changing anything else. As a result, there’s an alternative implementation that can be used:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class OSGiSupplier<T> implements Supplier<T> {
  private final BundleContext context;
  private final ServiceReference<T> serviceReference;
  private OSGiSupplier(Class<T> target, Class<?> source) {
    if (target == null) {
      throw new IllegalArgumentException("Target cannot be null");
    }
    if (source == null) {
      throw new IllegalArgumentException("Source cannot be null");
    }
    Bundle bundle = FrameworkUtil.getBundle(source);
    BundleContext context = bundle == null ? null : bundle.getBundleContext();
    if (context == null) {
      throw new IllegalArgumentException(
       "Unable to acquire bundle context for " + source.getCanonicalName());
    }
    this.context = context;
    this.serviceReference = context.getServiceReference(target);
  }
  public static <T> OSGiSupplier<T> supply(Class<T> target, Class<?> source) {
    return new OSGiSupplier<>(target, source);
  }
  @Override
  public T get() {
    try {
      T service = context.getService(serviceReference);
      if (service != null) {
        context.ungetService(serviceReference);
      }
      return service;
    } catch (Throwable t) {
      return null;
    }
  }
}

The API for using this is almost identical:

1
2
3
4
private final Supplier<DebugOptions> options =
 OSGiSupplier.supply(DebugOptions.class, getClass());

private final boolean DEBUG = options.get().getBooleanOption(...)

See the difference? It’s the type of supplier we are using. Otherwise the field type and use case is identical. That makes it easy to switch between the two; in fact, we could wrap this in another supplier if we wanted to default to everything being false or null:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DebugOptionsWrapper implements Supplier<DebugOptions> {
  private final Supplier<DebugOptions> delegate;
  public DebugOptionsWrapper(Supplier<DebugOptions> delegate) {
    this.delegate = delegate;
  }
  @Override
  public DebugOptions get() {
    DebugOptions options = delegate.get();
    if(options == null) {
      return new DebugOptions() { ... } // Empty implementation
    } else {
      return options;
    }
  }
}

This could be wrapped to prevent any NullPointerException being thrown if the service isn’t present:

1
2
private final Supplier<DebugOptions> options =
 new DebugOptionsWrapper(OSGiSupplier.supply(DebugOptions.class, getClass()));

The move to Java 8 facilitates these kinds of improvements, and should be part of Eclipse. The question is, where should the above go? Some shared package would make sense, but does this belong in org.eclipse.core.runtime or org.eclipse.equinox.util? Does this even make sense to add to Eclipse? Your thoughts are welcome; reach out to me @alblue.

Eclipse Night London

2015, eclipse

On 16 September I gave a presentation at Eclipse Night London on how to write bad Eclipse plug-ins. The goal of this presentation is to show how developers can create consistently bad user experience and how to use the Eclipse APIs to degrade performance and frustrate the user.

Of course, this is indended to be a toungue-in-cheek presentation and aims to show how certain design decisions can help or hinder the experience of an Eclipse user, and aims to show what not to do.

The Eclipse Night London was organised by Tracy Miranda and was held at the Stackoverflow offices in central London, near Old Street tube station.

The presentation is available at SpeakerDeck and I re-recorded an audio transcribed version for posterity at Vimeo. Enjoy!