In a presentation I gave at EclipseCon Europe in 2016, I noted that there were prolems when using ServiceTracker
and on slide 37 of my presentation noted that:
ServiceTracker.open()
is a blocking callServiceTracker.open()
results in DS activating services
Unfortunately, not everyone agrees because it seems insane that ServiceTracker
should do this.
Unfortunately, ServiceTracker
is insane.
The advantage of Declarative Services (aka SCR, although no-one calls it that) is that you can register services declaratively, but more importantly, the DS runtime will present the existence of the service but defer instantiation of the component until it’s first requested.
The great thing about this is that you can have a service which does many class loads or timely actions and defer its use until the service is actually needed. If your service isn’t required, then you don’t pay the cost for instantiating that service. I don’t think there’s any debate that this is a Good Thing and everyone, so far, is happy.
Problem
The problem, specifically when using ServiceTracker
, is that you have to do a two-step process to use it:
- You create a
ServiceTracker
for your particular service class - You call
open()
on it to start looking for services - Time passes
- You acquire the service form the
ServiceTracker
to do something with it
There is a generally held mistaken belief that the DS component is not
instantiated until you hit step 4 in the above. After all, if you’re calling
the service from another component – or even looking up the ServiceReference
yourself – that’s what would happen.
What actually happens is that the DS component is instantiated in step 2
above. That’s because the open()
call – which is nicely thread-safe by the
way, in the way that getService()
isn’t – starts looking for services, and
then caches the InitialTracked
service, which causes DS to instantiate the
component for you. Since most DS components often have a default, no-arg
constructor, this generally misses most people’s attention.
If your component’s constructor – or more importantly, the fields therein,
cause many classes to be loaded or perform substantial work or calculation, the
fact that you’re hitting a ServiceTracker.open()
synchronized call can take
some non-trivial amount of time. And since this is typically in an
Activator.start()
method, it means that your nicely delay-until-its-needed
component is now on the critical path of this bundle’s start-up, despite not
actually needing the service right now.
This is one of the main problems in Eclipse’s start-up; many, many thousands of
classes are loaded too eagerly. I’ve been working over the years to try and
reduce the problem but it’s an uphill struggle and bad patterns (particularly
the use of Activator
) are endemic in a non-trivial subset of the Eclipse
ecosystem. Of course, there are many fine and historical reasons why this is
the case, not the least of which is that we didn’t start shipping DS in the
Eclipse runtime until fairly recently.
Repo repro
Of course, when you point this out, not everyone is aware of this subtle behaviour. And while opinions may differ, code does not. I have put together a sample project which has two bundles:
- Client, which has an
Activator
(yeah I know, I’m using it to make a point) that uses aServiceTracker
to look forRunnable
instances - Runner, which has a DS component that provides a
Runnable
interface
When launched together, as soon as the ServiceTracker.open()
method is called, you can see the console printing "Component has been instantiated"
message. This is despite the Client
bundle never actually using the service that the ServiceTracker
causes to be obtained.
If you run it with the system property -DdisableOpen=true
, the
ServiceTracker.open()
statement is not called, and the component is not
instantiated.
This is a non-trivial reason as to why Eclipse startup can be slow. There are
many, many uses of ServiceTracker
to reach out to other parts of the system,
and regardless of whether these are lazy DS components or have been actively
instantiated, the use of ServiceTracker.open()
causes them to all be eagerly
activated, even before they’re needed. We can migrate Eclipse’s services to DS
(and in fact, I’m working on doing just that) but until we eliminate the
ServiceTracker
from various Activators, we won’t see the benefit.
The code in the github repository essentially boils down to:
public void start(BundleContext bundleContext) throws Exception {
serviceTracker = new ServiceTracker<>(bundleContext, Runnable.class, null);
if (!Boolean.getBoolean("disableOpen")) {
serviceTracker.open(); // This will cause a DS component to be instantiated even though we don't use it
}
}
Unfortunately, there’s no way to use ServiceTracker
to listen to lazily activated services, and as an OSGi standard, the behaviour is baked in to it.
Fortunately, there’s a lighter-weight tracker you can use called
ServiceCaller
– but that’s a topic for another blog post.
Summary
Using ServiceTracker.open()
will cause lazily instantiated DS components to
be activated eagerly, before the service is used. Instead of using
ServiceTracker
, try moving your service out to a DS component, and then DS
will do the right thing.