Bug 529639 Avoid extra sorts on start-level changes

Check the timestamp of the module database,
if it has not changed do not resort the modules
each active framework start-level change.

Change-Id: I7b75999e72bbef1479d7143c08a92b456c7c6ef0
Signed-off-by: Thomas Watson <tjwatson@us.ibm.com>
diff --git a/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/bundles/SystemBundleTests.java b/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/bundles/SystemBundleTests.java
index 3dc4f7e..bda5a64 100755
--- a/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/bundles/SystemBundleTests.java
+++ b/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/bundles/SystemBundleTests.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2008, 2017 IBM Corporation and others.
+ * Copyright (c) 2008, 2018 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
@@ -46,6 +46,7 @@
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.jar.Attributes;
 import java.util.jar.JarEntry;
@@ -75,6 +76,7 @@
 import org.osgi.framework.BundleListener;
 import org.osgi.framework.Constants;
 import org.osgi.framework.FrameworkEvent;
+import org.osgi.framework.FrameworkListener;
 import org.osgi.framework.InvalidSyntaxException;
 import org.osgi.framework.ServiceReference;
 import org.osgi.framework.ServiceRegistration;
@@ -86,6 +88,8 @@
 import org.osgi.framework.launch.Framework;
 import org.osgi.framework.launch.FrameworkFactory;
 import org.osgi.framework.namespace.NativeNamespace;
+import org.osgi.framework.startlevel.BundleStartLevel;
+import org.osgi.framework.startlevel.FrameworkStartLevel;
 import org.osgi.framework.wiring.BundleCapability;
 import org.osgi.framework.wiring.BundleRequirement;
 import org.osgi.framework.wiring.BundleRevision;
@@ -3306,6 +3310,160 @@
 		Assert.assertEquals("Wrong number of capabilities", capCount1, capCount2);
 	}
 
+	public void testStartLevelSorting() throws IOException, InterruptedException {
+		File config = OSGiTestsActivator.getContext().getDataFile(getName()); //$NON-NLS-1$
+		Map configuration = new HashMap();
+		configuration.put(Constants.FRAMEWORK_STORAGE, config.getAbsolutePath());
+		Equinox equinox = null;
+		final int numBundles = 200;
+		final File[] testBundleFiles = createBundles(new File(config, "testBundles"), numBundles);
+		try {
+			equinox = new Equinox(configuration);
+			equinox.start();
+			final List<Bundle> testBundles = Collections.synchronizedList(new ArrayList<Bundle>());
+			final List<Bundle> startedBundles = Collections.synchronizedList(new ArrayList<Bundle>());
+			for (int i = 0; i < numBundles / 2; i++) {
+				Bundle b = equinox.getBundleContext().installBundle("reference:file:///" + testBundleFiles[i].getAbsolutePath());
+				b.adapt(BundleStartLevel.class).setStartLevel(i + 2);
+				testBundles.add(b);
+				b.start();
+			}
+			final Equinox finalEquinox = equinox;
+
+			BundleListener initialListener = new SynchronousBundleListener() {
+				AtomicBoolean reverseStartLevel = new AtomicBoolean();
+				AtomicBoolean installBundles = new AtomicBoolean();
+
+				@Override
+				public void bundleChanged(BundleEvent event) {
+					Bundle eBundle = event.getBundle();
+					String bsn = eBundle.getSymbolicName();
+					if (!bsn.startsWith("bundle-b") || event.getType() != BundleEvent.STARTED) {
+						return;
+					}
+					startedBundles.add(eBundle);
+					if (reverseStartLevel.compareAndSet(false, true)) {
+						for (int i = numBundles / 4, j = numBundles; i < numBundles / 2; i++, j--) {
+							BundleStartLevel tbsl = testBundles.get(i).adapt(BundleStartLevel.class);
+							tbsl.setStartLevel(j + 2 + numBundles);
+						}
+					} else if (installBundles.compareAndSet(false, true)) {
+						for (int i = numBundles / 2; i < numBundles; i++) {
+							try {
+								Bundle b = finalEquinox.getBundleContext().installBundle("reference:file:///" + testBundleFiles[i].getAbsolutePath());
+								b.adapt(BundleStartLevel.class).setStartLevel(i + 2);
+								testBundles.add(b);
+								b.start();
+							} catch (BundleException e) {
+								// do nothing
+							}
+						}
+					}
+				}
+			};
+
+			equinox.getBundleContext().addBundleListener(initialListener);
+			long startTime = System.currentTimeMillis();
+			final CountDownLatch waitForStartLevel = new CountDownLatch(1);
+			equinox.adapt(FrameworkStartLevel.class).setStartLevel(numBundles * 3, new FrameworkListener() {
+
+				@Override
+				public void frameworkEvent(FrameworkEvent event) {
+					waitForStartLevel.countDown();
+				}
+			});
+			waitForStartLevel.await(20, TimeUnit.SECONDS);
+			System.out.println("Start time: " + (System.currentTimeMillis() - startTime));
+
+			assertEquals("Did not finish start level setting.", 0, waitForStartLevel.getCount());
+			assertEquals("Did not install all the expected bundles.", numBundles, testBundles.size());
+
+			List<Bundle> expectedStartOrder = new ArrayList<Bundle>();
+			for (int i = 0; i < numBundles / 4; i++) {
+				expectedStartOrder.add(testBundles.get(i));
+			}
+			for (int i = numBundles / 2; i < numBundles; i++) {
+				expectedStartOrder.add(testBundles.get(i));
+			}
+			for (int i = (numBundles / 2) - 1; i >= numBundles / 4; i--) {
+				expectedStartOrder.add(testBundles.get(i));
+			}
+
+			assertEquals("Size on expected order is wrong.", numBundles, expectedStartOrder.size());
+			for (int i = 0; i < numBundles; i++) {
+				assertEquals("Wrong bundle at: " + i, expectedStartOrder.get(i), startedBundles.get(i));
+			}
+
+			equinox.getBundleContext().removeBundleListener(initialListener);
+
+			final List<Bundle> stoppedBundles = Collections.synchronizedList(new ArrayList<Bundle>());
+			BundleListener shutdownListener = new SynchronousBundleListener() {
+				AtomicBoolean reverseStartLevel = new AtomicBoolean();
+				AtomicBoolean uninstallBundles = new AtomicBoolean();
+				AtomicBoolean inUninstall = new AtomicBoolean();
+
+				@Override
+				public void bundleChanged(BundleEvent event) {
+					if (inUninstall.get()) {
+						return;
+					}
+					Bundle eBundle = event.getBundle();
+					String bsn = eBundle.getSymbolicName();
+					if (!bsn.startsWith("bundle-b") || event.getType() != BundleEvent.STOPPED) {
+						return;
+					}
+					stoppedBundles.add(eBundle);
+					if (reverseStartLevel.compareAndSet(false, true)) {
+						for (int i = numBundles / 2, j = numBundles - 1; i < numBundles; i++, j--) {
+							BundleStartLevel tbsl = testBundles.get(i).adapt(BundleStartLevel.class);
+							tbsl.setStartLevel(j + 2);
+						}
+					} else if (uninstallBundles.compareAndSet(false, true)) {
+						for (int i = 0; i < numBundles / 4; i++) {
+							try {
+								inUninstall.set(true);
+								testBundles.get(i).uninstall();
+							} catch (BundleException e) {
+								// do nothing
+							} finally {
+								inUninstall.set(false);
+							}
+						}
+					}
+				}
+			};
+			equinox.getBundleContext().addBundleListener(shutdownListener);
+
+			List<Bundle> expectedStopOrder = new ArrayList<Bundle>(expectedStartOrder);
+			Collections.reverse(expectedStopOrder);
+			Collections.reverse(expectedStopOrder.subList(numBundles / 4, 3 * (numBundles / 4)));
+			expectedStopOrder = new ArrayList(expectedStopOrder.subList(0, 3 * (numBundles / 4)));
+
+			long stopTime = System.currentTimeMillis();
+			equinox.stop();
+			equinox.waitForStop(20000);
+			System.out.println("Stop time: " + (System.currentTimeMillis() - stopTime));
+
+			assertEquals("Size on expected order is wrong.", expectedStopOrder.size(), stoppedBundles.size());
+			for (int i = 0; i < expectedStopOrder.size(); i++) {
+				assertEquals("Wrong bundle at: " + i, expectedStopOrder.get(i), stoppedBundles.get(i));
+			}
+		} catch (BundleException e) {
+			fail("Failed init", e);
+		} finally {
+			try {
+				if (equinox != null) {
+					equinox.stop();
+					equinox.waitForStop(1000);
+				}
+			} catch (BundleException e) {
+				fail("Failed to stop framework.", e);
+			} catch (InterruptedException e) {
+				fail("Failed to stop framework.", e);
+			}
+		}
+	}
+
 	// Disabled because the test is too much for the build machines
 	public void disable_testBundleIDLock() {
 		File config = OSGiTestsActivator.getContext().getDataFile(getName()); //$NON-NLS-1$
diff --git a/bundles/org.eclipse.osgi/container/src/org/eclipse/osgi/container/ModuleContainer.java b/bundles/org.eclipse.osgi/container/src/org/eclipse/osgi/container/ModuleContainer.java
index 5756be2..62934f5 100644
--- a/bundles/org.eclipse.osgi/container/src/org/eclipse/osgi/container/ModuleContainer.java
+++ b/bundles/org.eclipse.osgi/container/src/org/eclipse/osgi/container/ModuleContainer.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2012, 2017 IBM Corporation and others.
+ * Copyright (c) 2012, 2018 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
@@ -12,16 +12,30 @@
 
 import java.security.AccessController;
 import java.security.PrivilegedAction;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
-import org.eclipse.osgi.container.Module.*;
+import org.eclipse.osgi.container.Module.StartOptions;
+import org.eclipse.osgi.container.Module.State;
+import org.eclipse.osgi.container.Module.StopOptions;
 import org.eclipse.osgi.container.ModuleContainerAdaptor.ContainerEvent;
 import org.eclipse.osgi.container.ModuleContainerAdaptor.ModuleEvent;
 import org.eclipse.osgi.container.ModuleDatabase.Sort;
 import org.eclipse.osgi.container.ModuleRequirement.DynamicModuleRequirement;
-import org.eclipse.osgi.framework.eventmgr.*;
+import org.eclipse.osgi.framework.eventmgr.CopyOnWriteIdentityMap;
+import org.eclipse.osgi.framework.eventmgr.EventDispatcher;
+import org.eclipse.osgi.framework.eventmgr.EventManager;
+import org.eclipse.osgi.framework.eventmgr.ListenerQueue;
 import org.eclipse.osgi.framework.util.SecureAction;
 import org.eclipse.osgi.internal.container.InternalUtils;
 import org.eclipse.osgi.internal.container.LockSet;
@@ -33,11 +47,24 @@
 import org.eclipse.osgi.service.debug.DebugOptions;
 import org.eclipse.osgi.service.debug.DebugOptionsListener;
 import org.eclipse.osgi.util.NLS;
-import org.osgi.framework.*;
-import org.osgi.framework.namespace.*;
+import org.osgi.framework.AdminPermission;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.Constants;
+import org.osgi.framework.FrameworkListener;
+import org.osgi.framework.Version;
+import org.osgi.framework.namespace.HostNamespace;
+import org.osgi.framework.namespace.IdentityNamespace;
+import org.osgi.framework.namespace.PackageNamespace;
 import org.osgi.framework.startlevel.FrameworkStartLevel;
-import org.osgi.framework.wiring.*;
-import org.osgi.resource.*;
+import org.osgi.framework.wiring.BundleCapability;
+import org.osgi.framework.wiring.BundleRevision;
+import org.osgi.framework.wiring.FrameworkWiring;
+import org.osgi.resource.Namespace;
+import org.osgi.resource.Requirement;
+import org.osgi.resource.Resource;
+import org.osgi.resource.Wire;
 import org.osgi.service.resolver.ResolutionException;
 
 /**
@@ -1575,6 +1602,9 @@
 					}
 					// Note that we must get a new list of modules each time;
 					// this is because additional modules could have been installed from the previous start-level
+					// but only do this if the module database has changed!!
+					List<Module> sorted = null;
+					long currentTimestamp = Long.MIN_VALUE;
 					if (newStartLevel > currentSL) {
 						for (int i = currentSL; i < newStartLevel; i++) {
 							int toStartLevel = i + 1;
@@ -1582,7 +1612,16 @@
 							if (debugStartLevel) {
 								Debug.println("StartLevel: incremented active start level to; " + toStartLevel); //$NON-NLS-1$
 							}
-							incStartLevel(toStartLevel, moduleDatabase.getSortedModules(Sort.BY_START_LEVEL));
+							if (sorted == null || currentTimestamp != moduleDatabase.getTimestamp()) {
+								moduleDatabase.readLock();
+								try {
+									sorted = moduleDatabase.getSortedModules(Sort.BY_START_LEVEL);
+									currentTimestamp = moduleDatabase.getTimestamp();
+								} finally {
+									moduleDatabase.readUnlock();
+								}
+							}
+							incStartLevel(toStartLevel, sorted);
 						}
 					} else {
 						for (int i = currentSL; i > newStartLevel; i--) {
@@ -1591,7 +1630,16 @@
 							if (debugStartLevel) {
 								Debug.println("StartLevel: decremented active start level to " + toStartLevel); //$NON-NLS-1$
 							}
-							decStartLevel(toStartLevel, moduleDatabase.getSortedModules(Sort.BY_START_LEVEL, Sort.BY_DEPENDENCY));
+							if (sorted == null || currentTimestamp != moduleDatabase.getTimestamp()) {
+								moduleDatabase.readLock();
+								try {
+									sorted = moduleDatabase.getSortedModules(Sort.BY_START_LEVEL, Sort.BY_DEPENDENCY);
+									currentTimestamp = moduleDatabase.getTimestamp();
+								} finally {
+									moduleDatabase.readUnlock();
+								}
+							}
+							decStartLevel(toStartLevel, sorted);
 						}
 					}
 					if (currentSL > 0 && newStartLevel > 0) {