Bug 576736 - Run API analysis in a job

- added "Run API analysis builder as job" option for PDE
- this option is disabled by default
- enabling it would run API analysis builder tasks parallel to the
running Java build
- for full workspace build this avoids the problem that Java builder of
next projects to build have to wait for API build of the current project

Change-Id: Icac0487ba3dc14efc340afed43afd9407a2c7d1d
Reviewed-on: https://git.eclipse.org/r/c/pde/eclipse.pde.ui/+/186655
Tested-by: PDE Bot <pde-bot@eclipse.org>
Tested-by: Vikas Chandra <Vikas.Chandra@in.ibm.com>
Reviewed-by: Vikas Chandra <Vikas.Chandra@in.ibm.com>
diff --git a/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/ApiAnalysisApplication.java b/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/ApiAnalysisApplication.java
index beee248..287dadb 100644
--- a/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/ApiAnalysisApplication.java
+++ b/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/ApiAnalysisApplication.java
@@ -113,6 +113,7 @@
 			desc.setAutoBuilding(false);
 			ResourcesPlugin.getWorkspace().setDescription(desc);
 			PDECore.getDefault().getPreferencesManager().setValue(ICoreConstants.DISABLE_API_ANALYSIS_BUILDER, false);
+			PDECore.getDefault().getPreferencesManager().setValue(ICoreConstants.RUN_API_ANALYSIS_AS_JOB, false);
 
 			Request args = Request
 					.readFromArgs((String[]) context.getArguments().get(IApplicationContext.APPLICATION_ARGS));
diff --git a/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/ApiDescription.java b/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/ApiDescription.java
index fdd0a4e..84a3f78 100644
--- a/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/ApiDescription.java
+++ b/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/ApiDescription.java
@@ -69,7 +69,7 @@
 	/**
 	 * Whether this description needs saving
 	 */
-	private boolean fModified = false;
+	private volatile boolean fModified;
 
 	/**
 	 * A comparator for {@link ManifestNode}s. Used while visiting child nodes
@@ -634,7 +634,7 @@
 	/**
 	 * Marks the description as modified
 	 */
-	protected synchronized void modified() {
+	protected void modified() {
 		fModified = true;
 	}
 
@@ -643,7 +643,7 @@
 	 *
 	 * @return
 	 */
-	protected synchronized boolean isModified() {
+	protected boolean isModified() {
 		return fModified;
 	}
 
@@ -653,7 +653,7 @@
 	 * @param mod
 	 * @return
 	 */
-	protected synchronized void setModified(boolean mod) {
+	protected void setModified(boolean mod) {
 		fModified = mod;
 	}
 
diff --git a/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/ProjectApiDescription.java b/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/ProjectApiDescription.java
index e8c48d2..3b5475f 100644
--- a/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/ProjectApiDescription.java
+++ b/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/ProjectApiDescription.java
@@ -77,7 +77,7 @@
 	/**
 	 * Whether a package refresh is in progress
 	 */
-	private boolean fRefreshingInProgress = false;
+	private volatile boolean fRefreshingInProgress;
 
 	/**
 	 * Associated manifest file
@@ -90,7 +90,7 @@
 	 * traversing the cached nodes, rather than traversing the java model
 	 * elements (effectively building the cache).
 	 */
-	private boolean fInSynch = false;
+	private volatile boolean fInSynch;
 
 	/**
 	 * A node for a package.
@@ -168,7 +168,7 @@
 
 		long fTimeStamp = -1L;
 		long fBuildStamp = -1L;
-		private boolean fRefreshing = false;
+		private volatile boolean fRefreshing;
 
 		IType fType;
 
@@ -191,114 +191,124 @@
 		}
 
 		@Override
-		protected synchronized ManifestNode refresh() {
+		protected ManifestNode refresh() {
 			if (fRefreshing) {
-				if (ApiPlugin.DEBUG_API_DESCRIPTION) {
-					StringBuilder buffer = new StringBuilder();
-					buffer.append("Refreshing manifest node: "); //$NON-NLS-1$
-					buffer.append(this);
-					buffer.append(" aborted because a refresh is already in progress"); //$NON-NLS-1$
-					System.out.println(buffer.toString());
-				}
+				logRefreshing();
 				return this;
 			}
-			try {
-				fRefreshing = true;
-				int parentVis = resolveVisibility(parent);
-				if (VisibilityModifiers.isAPI(parentVis)) {
-					ICompilationUnit unit = fType.getCompilationUnit();
-					if (unit != null) {
-						IResource resource = null;
-						try {
-							resource = unit.getUnderlyingResource();
-						} catch (JavaModelException e) {
-							if (ApiPlugin.DEBUG_API_DESCRIPTION) {
-								StringBuilder buffer = new StringBuilder();
-								buffer.append("Failed to get underlying resource for compilation unit: "); //$NON-NLS-1$
-								buffer.append(unit);
-								System.out.println(buffer.toString());
-							}
-							// exception if the resource does not exist
-							if (!e.getJavaModelStatus().isDoesNotExist()) {
-								ApiPlugin.log(e.getStatus());
-								return this;
-							}
-						}
-						if (resource != null && resource.exists()) {
-							long stamp = resource.getModificationStamp();
-							if (stamp != fTimeStamp) {
-								// compute current CRC
-								CRCVisitor visitor = new CRCVisitor();
-								visitType(this, visitor);
-								long crc = visitor.getValue();
+			synchronized (this) {
+				if (fRefreshing) {
+					logRefreshing();
+					return this;
+				}
+				try {
+					fRefreshing = true;
+					int parentVis = resolveVisibility(parent);
+					if (VisibilityModifiers.isAPI(parentVis)) {
+						ICompilationUnit unit = fType.getCompilationUnit();
+						if (unit != null) {
+							IResource resource = null;
+							try {
+								resource = unit.getUnderlyingResource();
+							} catch (JavaModelException e) {
 								if (ApiPlugin.DEBUG_API_DESCRIPTION) {
 									StringBuilder buffer = new StringBuilder();
-									buffer.append("Resource has changed for type manifest node: "); //$NON-NLS-1$
-									buffer.append(this);
-									buffer.append(" tag scanning the new type"); //$NON-NLS-1$
-									buffer.append(" (CRC "); //$NON-NLS-1$
-									buffer.append(crc);
-									buffer.append(')');
+									buffer.append("Failed to get underlying resource for compilation unit: "); //$NON-NLS-1$
+									buffer.append(unit);
 									System.out.println(buffer.toString());
 								}
-								modified();
-								children.clear();
-								restrictions = RestrictionModifiers.NO_RESTRICTIONS;
-								fTimeStamp = resource.getModificationStamp();
-								try {
-									TagScanner.newScanner().scan(unit, ProjectApiDescription.this, getApiTypeContainer((IPackageFragmentRoot) fType.getPackageFragment().getParent()), null);
-								} catch (CoreException e) {
+								// exception if the resource does not exist
+								if (!e.getJavaModelStatus().isDoesNotExist()) {
 									ApiPlugin.log(e.getStatus());
+									return this;
 								}
-								// see if the description changed
-								visitor = new CRCVisitor();
-								visitType(this, visitor);
-								long crc2 = visitor.getValue();
-								if (crc != crc2) {
-									// update relative build time stamp
-									fBuildStamp = BuildStamps.getBuildStamp(resource.getProject());
+							}
+							if (resource != null && resource.exists()) {
+								long stamp = resource.getModificationStamp();
+								if (stamp != fTimeStamp) {
+									// compute current CRC
+									CRCVisitor visitor = new CRCVisitor();
+									visitType(this, visitor);
+									long crc = visitor.getValue();
 									if (ApiPlugin.DEBUG_API_DESCRIPTION) {
 										StringBuilder buffer = new StringBuilder();
-										buffer.append("CRC changed for type manifest node: "); //$NON-NLS-1$
+										buffer.append("Resource has changed for type manifest node: "); //$NON-NLS-1$
 										buffer.append(this);
+										buffer.append(" tag scanning the new type"); //$NON-NLS-1$
 										buffer.append(" (CRC "); //$NON-NLS-1$
-										buffer.append(crc2);
+										buffer.append(crc);
 										buffer.append(')');
 										System.out.println(buffer.toString());
 									}
+									modified();
+									children.clear();
+									restrictions = RestrictionModifiers.NO_RESTRICTIONS;
+									fTimeStamp = resource.getModificationStamp();
+									try {
+										TagScanner.newScanner().scan(unit, ProjectApiDescription.this, getApiTypeContainer((IPackageFragmentRoot) fType.getPackageFragment().getParent()), null);
+									} catch (CoreException e) {
+										ApiPlugin.log(e.getStatus());
+									}
+									// see if the description changed
+									visitor = new CRCVisitor();
+									visitType(this, visitor);
+									long crc2 = visitor.getValue();
+									if (crc != crc2) {
+										// update relative build time stamp
+										fBuildStamp = BuildStamps.getBuildStamp(resource.getProject());
+										if (ApiPlugin.DEBUG_API_DESCRIPTION) {
+											StringBuilder buffer = new StringBuilder();
+											buffer.append("CRC changed for type manifest node: "); //$NON-NLS-1$
+											buffer.append(this);
+											buffer.append(" (CRC "); //$NON-NLS-1$
+											buffer.append(crc2);
+											buffer.append(')');
+											System.out.println(buffer.toString());
+										}
+									}
 								}
+							} else {
+								if (ApiPlugin.DEBUG_API_DESCRIPTION) {
+									StringBuilder buffer = new StringBuilder();
+									buffer.append("Underlying resource for the type manifest node: "); //$NON-NLS-1$
+									buffer.append(this);
+									buffer.append(" does not exist or is null"); //$NON-NLS-1$
+									System.out.println(buffer.toString());
+								}
+								// element has been removed
+								modified();
+								parent.children.remove(element);
+								return null;
 							}
 						} else {
 							if (ApiPlugin.DEBUG_API_DESCRIPTION) {
 								StringBuilder buffer = new StringBuilder();
-								buffer.append("Underlying resource for the type manifest node: "); //$NON-NLS-1$
+								buffer.append("Failed to look up compilation unit for "); //$NON-NLS-1$
+								buffer.append(fType);
+								buffer.append(" refreshing type manifest node: "); //$NON-NLS-1$
 								buffer.append(this);
-								buffer.append(" does not exist or is null"); //$NON-NLS-1$
 								System.out.println(buffer.toString());
 							}
-							// element has been removed
-							modified();
-							parent.children.remove(element);
-							return null;
+							// TODO: binary type
 						}
 					} else {
-						if (ApiPlugin.DEBUG_API_DESCRIPTION) {
-							StringBuilder buffer = new StringBuilder();
-							buffer.append("Failed to look up compilation unit for "); //$NON-NLS-1$
-							buffer.append(fType);
-							buffer.append(" refreshing type manifest node: "); //$NON-NLS-1$
-							buffer.append(this);
-							System.out.println(buffer.toString());
-						}
-						// TODO: binary type
+						// don't scan internal types
 					}
-				} else {
-					// don't scan internal types
+				} finally {
+					fRefreshing = false;
 				}
-			} finally {
-				fRefreshing = false;
+				return this;
 			}
-			return this;
+		}
+
+		private void logRefreshing() {
+			if (ApiPlugin.DEBUG_API_DESCRIPTION) {
+				StringBuilder buffer = new StringBuilder();
+				buffer.append("Refreshing manifest node: "); //$NON-NLS-1$
+				buffer.append(this);
+				buffer.append(" aborted because a refresh is already in progress"); //$NON-NLS-1$
+				System.out.println(buffer.toString());
+			}
 		}
 
 		@Override
@@ -555,47 +565,57 @@
 	/**
 	 * Refreshes package nodes if required.
 	 */
-	synchronized void refreshPackages() {
+	void refreshPackages() {
 		if (fRefreshingInProgress) {
-			if (ApiPlugin.DEBUG_API_DESCRIPTION) {
-				StringBuilder buffer = new StringBuilder();
-				buffer.append("Refreshing manifest node: "); //$NON-NLS-1$
-				buffer.append(this);
-				buffer.append(" aborted because a refresh is already in progress"); //$NON-NLS-1$
-				System.out.println(buffer.toString());
-			}
+			logPackafesRefresh();
 			return;
 		}
-		// check if in synch
-		if (fManifestFile == null || (fManifestFile.getModificationStamp() != fPackageTimeStamp)) {
-			try {
-				modified();
-				fRefreshingInProgress = true;
-				// set all existing packages to PRIVATE (could clear
-				// the map, but it would be less efficient)
-				Iterator<ManifestNode> iterator = fPackageMap.values().iterator();
-				while (iterator.hasNext()) {
-					PackageNode node = (PackageNode) iterator.next();
-					node.visibility = VisibilityModifiers.PRIVATE;
-				}
-				fManifestFile = getJavaProject().getProject().getFile(JarFile.MANIFEST_NAME);
-				if (fManifestFile.exists()) {
-					try {
-						IPackageFragment[] fragments = getLocalPackageFragments();
-						Set<String> names = new HashSet<>();
-						for (IPackageFragment fragment : fragments) {
-							names.add(fragment.getElementName());
-						}
-						ProjectComponent component = getApiComponent();
-						BundleComponent.initializeApiDescription(this, component.getBundleDescription(), names);
-						fPackageTimeStamp = fManifestFile.getModificationStamp();
-					} catch (CoreException e) {
-						ApiPlugin.log(e.getStatus());
-					}
-				}
-			} finally {
-				fRefreshingInProgress = false;
+		synchronized (this) {
+			if (fRefreshingInProgress) {
+				logPackafesRefresh();
+				return;
 			}
+			// check if in synch
+			if (fManifestFile == null || (fManifestFile.getModificationStamp() != fPackageTimeStamp)) {
+				try {
+					modified();
+					fRefreshingInProgress = true;
+					// set all existing packages to PRIVATE (could clear
+					// the map, but it would be less efficient)
+					Iterator<ManifestNode> iterator = fPackageMap.values().iterator();
+					while (iterator.hasNext()) {
+						PackageNode node = (PackageNode) iterator.next();
+						node.visibility = VisibilityModifiers.PRIVATE;
+					}
+					fManifestFile = getJavaProject().getProject().getFile(JarFile.MANIFEST_NAME);
+					if (fManifestFile.exists()) {
+						try {
+							IPackageFragment[] fragments = getLocalPackageFragments();
+							Set<String> names = new HashSet<>();
+							for (IPackageFragment fragment : fragments) {
+								names.add(fragment.getElementName());
+							}
+							ProjectComponent component = getApiComponent();
+							BundleComponent.initializeApiDescription(this, component.getBundleDescription(), names);
+							fPackageTimeStamp = fManifestFile.getModificationStamp();
+						} catch (CoreException e) {
+							ApiPlugin.log(e.getStatus());
+						}
+					}
+				} finally {
+					fRefreshingInProgress = false;
+				}
+			}
+		}
+	}
+
+	private void logPackafesRefresh() {
+		if (ApiPlugin.DEBUG_API_DESCRIPTION) {
+			StringBuilder buffer = new StringBuilder();
+			buffer.append("Refreshing manifest node: "); //$NON-NLS-1$
+			buffer.append(this);
+			buffer.append(" aborted because a refresh is already in progress"); //$NON-NLS-1$
+			System.out.println(buffer.toString());
 		}
 	}
 
@@ -704,7 +724,7 @@
 	 * Notes that the underlying project has changed in some way and that the
 	 * description cache is no longer in synch with the project.
 	 */
-	public synchronized void projectChanged() {
+	public void projectChanged() {
 		fInSynch = false;
 	}
 
diff --git a/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/builder/ApiAnalysisBuilder.java b/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/builder/ApiAnalysisBuilder.java
index 95b4511..7d4f32e 100644
--- a/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/builder/ApiAnalysisBuilder.java
+++ b/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/builder/ApiAnalysisBuilder.java
@@ -26,6 +26,7 @@
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
+import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.jar.JarFile;
 
 import org.eclipse.core.resources.IFile;
@@ -36,6 +37,7 @@
 import org.eclipse.core.resources.IWorkspaceRoot;
 import org.eclipse.core.resources.IncrementalProjectBuilder;
 import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.resources.WorkspaceJob;
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IPath;
 import org.eclipse.core.runtime.IProgressMonitor;
@@ -44,6 +46,8 @@
 import org.eclipse.core.runtime.Path;
 import org.eclipse.core.runtime.Status;
 import org.eclipse.core.runtime.SubMonitor;
+import org.eclipse.core.runtime.jobs.ISchedulingRule;
+import org.eclipse.core.runtime.jobs.Job;
 import org.eclipse.jdt.core.IClasspathAttribute;
 import org.eclipse.jdt.core.IClasspathEntry;
 import org.eclipse.jdt.core.ICompilationUnit;
@@ -178,6 +182,8 @@
 	 */
 	private BuildState buildstate = null;
 
+	private ConcurrentLinkedQueue<Runnable> markersQueue = new ConcurrentLinkedQueue<>();
+
 	/**
 	 * Bug 549838:  In case auto-building on a API tools settings change  is not desired,
 	 * specify VM property: {@code -Dorg.eclipse.disableAutoBuildOnSettingsChange=true}
@@ -190,6 +196,20 @@
 	 * @param resource
 	 */
 	void cleanupMarkers(IResource resource) {
+		if (isRunningAsJob()) {
+			ApiAnalysisMarkersJob job = new ApiAnalysisMarkersJob(() -> cleanupMarkersInternally(resource));
+			job.schedule();
+		} else {
+			cleanupMarkersInternally(resource);
+		}
+	}
+
+	/**
+	 * Cleans up markers associated with API Tools on the given resource.
+	 *
+	 * @param resource
+	 */
+	void cleanupMarkersInternally(IResource resource) {
 		cleanUnusedFilterMarkers(resource);
 		cleanupUsageMarkers(resource);
 		cleanupCompatibilityMarkers(resource);
@@ -342,6 +362,13 @@
 	}
 
 	@Override
+	public ISchedulingRule getRule(int kind, Map<String, String> args) {
+		// TODO probably we don't need even this and can return null if we are running as job
+		// if(isRunningAsJob()) return null;
+		return currentproject;
+	}
+
+	@Override
 	protected IProject[] build(int kind, Map<String, String> args, IProgressMonitor monitor) throws CoreException {
 		PDEPreferencesManager prefs = PDECore.getDefault().getPreferencesManager();
 		boolean disableAPIAnalysisBuilder = prefs.getBoolean(ICoreConstants.DISABLE_API_ANALYSIS_BUILDER);
@@ -357,7 +384,6 @@
 		if (ApiPlugin.DEBUG_BUILDER) {
 			System.out.println("\nApiAnalysisBuilder: Starting build of " + this.currentproject.getName() + " @ " + new Date(System.currentTimeMillis())); //$NON-NLS-1$ //$NON-NLS-2$
 		}
-		SubMonitor localMonitor = SubMonitor.convert(monitor, BuilderMessages.api_analysis_builder, 8);
 		IApiBaseline wbaseline = ApiPlugin.getDefault().getApiBaselineManager().getWorkspaceBaseline();
 		if (wbaseline == null) {
 			if (ApiPlugin.DEBUG_BUILDER) {
@@ -366,85 +392,93 @@
 			return NO_PROJECTS;
 		}
 		final IProject[] projects = getRequiredProjects(true);
+		if (kind != FULL_BUILD && kind != AUTO_BUILD && kind != INCREMENTAL_BUILD) {
+			return projects;
+		}
+		boolean fullBuild = kind == FULL_BUILD;
+		boolean runAsJob = prefs.getBoolean(ICoreConstants.RUN_API_ANALYSIS_AS_JOB);
+		if (runAsJob) {
+			ApiAnalysisJob job = new ApiAnalysisJob(BuilderMessages.api_analysis_builder, currentproject, fullBuild,
+					wbaseline, projects);
+			job.cancelSimilarJobs(fullBuild);
+			job.schedule(100);
+		} else {
+			work(fullBuild, wbaseline, projects, monitor);
+		}
+		return projects;
+	}
+
+	protected void work(final boolean fullBuild, IApiBaseline wbaseline, IProject[] projects, IProgressMonitor monitor)
+			throws CoreException {
+		SubMonitor localMonitor = SubMonitor.convert(monitor, BuilderMessages.api_analysis_builder, 8);
+
 		IApiBaseline baseline = ApiPlugin.getDefault().getApiBaselineManager().getDefaultApiBaseline();
 		try {
 			SubMonitor switchMonitor = localMonitor.split(4);
-			switch (kind) {
-				case FULL_BUILD: {
-					if (ApiPlugin.DEBUG_BUILDER) {
-						System.out.println("ApiAnalysisBuilder: Performing full build as requested"); //$NON-NLS-1$
-					}
-					buildAll(baseline, wbaseline, switchMonitor);
-					break;
+			if (fullBuild) {
+				if (ApiPlugin.DEBUG_BUILDER) {
+					System.out.println("ApiAnalysisBuilder: Performing full build as requested"); //$NON-NLS-1$
 				}
-				case AUTO_BUILD:
-				case INCREMENTAL_BUILD: {
-					this.buildstate = BuildState.getLastBuiltState(currentproject);
-					if (this.buildstate == null) {
+				buildAll(baseline, wbaseline, switchMonitor);
+			} else {
+				this.buildstate = BuildState.getLastBuiltState(currentproject);
+				if (this.buildstate == null) {
+					buildAll(baseline, wbaseline, switchMonitor);
+				} else if (worthDoingFullBuild(projects)) {
+					buildAll(baseline, wbaseline, switchMonitor);
+				} else {
+					IResourceDelta[] deltas = getDeltas(projects);
+					if (deltas.length < 1) {
 						buildAll(baseline, wbaseline, switchMonitor);
-						break;
-					} else if (worthDoingFullBuild(projects)) {
-						buildAll(baseline, wbaseline, switchMonitor);
-						break;
 					} else {
-						IResourceDelta[] deltas = getDeltas(projects);
-						if (deltas.length < 1) {
-							buildAll(baseline, wbaseline, switchMonitor);
-						} else {
-							IResourceDelta filters = null;
-							boolean full = false;
-							for (IResourceDelta delta : deltas) {
-								full = shouldFullBuild(delta);
-								if (full) {
-									break;
-								}
-								filters = delta.findMember(FILTER_PATH);
-								if (filters != null) {
-									switch (filters.getKind()) {
-										case IResourceDelta.ADDED:
-										case IResourceDelta.REMOVED: {
-											full = true;
-											break;
-										}
-										case IResourceDelta.CHANGED: {
-											full = (filters.getFlags() & (IResourceDelta.REPLACED | IResourceDelta.CONTENT)) > 0;
-											break;
-										}
-										default: {
-											break;
-										}
+						IResourceDelta filters = null;
+						boolean full = false;
+						for (IResourceDelta delta : deltas) {
+							full = shouldFullBuild(delta);
+							if (full) {
+								break;
+							}
+							filters = delta.findMember(FILTER_PATH);
+							if (filters != null) {
+								switch (filters.getKind()) {
+									case IResourceDelta.ADDED:
+									case IResourceDelta.REMOVED: {
+										full = true;
+										break;
 									}
-									if (full) {
+									case IResourceDelta.CHANGED: {
+										full = (filters.getFlags() & (IResourceDelta.REPLACED | IResourceDelta.CONTENT)) > 0;
+										break;
+									}
+									default: {
 										break;
 									}
 								}
-							}
-							if (full) {
-								if (ApiPlugin.DEBUG_BUILDER) {
-									System.out.println("ApiAnalysisBuilder: Performing full build since MANIFEST.MF or .api_filters was modified"); //$NON-NLS-1$
-								}
-								buildAll(baseline, wbaseline, switchMonitor);
-							} else {
-								switchMonitor.setWorkRemaining(2);
-								State state = (State) JavaModelManager.getJavaModelManager().getLastBuiltState(this.currentproject, switchMonitor.split(1));
-								if (state == null) {
-									buildAll(baseline, wbaseline, switchMonitor.split(1));
+								if (full) {
 									break;
 								}
+							}
+						}
+						if (full) {
+							if (ApiPlugin.DEBUG_BUILDER) {
+								System.out.println("ApiAnalysisBuilder: Performing full build since MANIFEST.MF or .api_filters was modified"); //$NON-NLS-1$
+							}
+							buildAll(baseline, wbaseline, switchMonitor);
+						} else {
+							switchMonitor.setWorkRemaining(2);
+							State state = (State) JavaModelManager.getJavaModelManager().getLastBuiltState(this.currentproject, switchMonitor.split(1));
+							if (state == null) {
+								buildAll(baseline, wbaseline, switchMonitor.split(1));
+							} else {
 								BuildState.setLastBuiltState(this.currentproject, null);
 								IncrementalApiBuilder builder = new IncrementalApiBuilder(this);
 								builder.build(baseline, wbaseline, deltas, state, this.buildstate, switchMonitor.split(1));
 							}
 						}
 					}
-					break;
-				}
-				default: {
-					break;
 				}
 			}
 			localMonitor.split(1);
-
 		} catch (OperationCanceledException oce) {
 			// do nothing, but don't forward it
 			// https://bugs.eclipse.org/bugs/show_bug.cgi?id=304315
@@ -469,9 +503,10 @@
 					// be built do not close
 					// the baselines yet, they might be re-read by another build
 					// cycle
-					if (baseline != null) {
-						baseline.close();
-					}
+					// TODO: this seem to be not needed anymore.
+					//	if (baseline != null) {
+					//		baseline.close();
+					//	}
 				}
 				localMonitor.split(1);
 				if (this.buildstate != null) {
@@ -517,7 +552,50 @@
 		if (ApiPlugin.DEBUG_BUILDER) {
 			System.out.println("ApiAnalysisBuilder: Finished build of " + this.currentproject.getName() + " @ " + new Date(System.currentTimeMillis())); //$NON-NLS-1$ //$NON-NLS-2$
 		}
-		return projects;
+	}
+
+	class ApiAnalysisJob extends Job {
+
+		private boolean fullBuild;
+		private IApiBaseline wbaseline;
+		private IProject[] projects;
+		private IProject project;
+
+		public ApiAnalysisJob(String name, IProject project, boolean fullBuild, IApiBaseline wbaseline,
+				IProject[] projects) {
+			super(name);
+			this.project = project;
+			this.fullBuild = fullBuild;
+			this.wbaseline = wbaseline;
+			this.projects = projects;
+			// Intentionally no rule set to allow run in parallel with build locking entire workspace
+			// setRule(project);
+		}
+
+		@Override
+		public IStatus run(IProgressMonitor monitor) {
+			try {
+				work(fullBuild, wbaseline, projects, monitor);
+			} catch (CoreException e) {
+				return e.getStatus();
+			}
+			return Status.OK_STATUS;
+		}
+
+		@Override
+		public boolean belongsTo(Object family) {
+			return super.belongsTo(family) || ApiAnalysisJob.class == family;
+		}
+
+		void cancelSimilarJobs(boolean fullBuild) {
+			Job[] jobs = Job.getJobManager().find(ApiAnalysisJob.class);
+			for (Job job : jobs) {
+				ApiAnalysisJob ajob = (ApiAnalysisJob) job;
+				if (fullBuild == ajob.fullBuild && project.equals(ajob.project)) {
+					job.cancel();
+				}
+			}
+		}
 	}
 
 	/**
@@ -721,14 +799,30 @@
 			}
 		}
 
+		Runnable task;
 		if (hasFatalProblem) {
-			cleanupMarkers(project);
-			IApiProblem problem = ApiProblemFactory.newFatalProblem(Path.EMPTY.toString(), new String[] { project.getName() }, IApiProblem.FATAL_JDT_BUILDPATH_PROBLEM);
-			createMarkerForProblem(IApiProblem.CATEGORY_FATAL_PROBLEM, IApiMarkerConstants.FATAL_PROBLEM_MARKER, problem);
-			return true;
+			task = () -> {
+				cleanupMarkers(project);
+				IApiProblem problem = ApiProblemFactory.newFatalProblem(Path.EMPTY.toString(), new String[] { project.getName() }, IApiProblem.FATAL_JDT_BUILDPATH_PROBLEM);
+				createMarkerForProblem(IApiProblem.CATEGORY_FATAL_PROBLEM, IApiMarkerConstants.FATAL_PROBLEM_MARKER, problem);
+			};
+		} else {
+			task = () -> cleanupFatalMarkers(project);
 		}
-		cleanupFatalMarkers(project);
-		return false;
+
+		boolean runAsJob = isRunningAsJob();
+		if (runAsJob) {
+			new ApiAnalysisMarkersJob(task).schedule();
+		} else {
+			task.run();
+		}
+		return hasFatalProblem;
+	}
+
+	private static boolean isRunningAsJob() {
+		PDEPreferencesManager prefs = PDECore.getDefault().getPreferencesManager();
+		boolean runAsJob = prefs.getBoolean(ICoreConstants.RUN_API_ANALYSIS_AS_JOB);
+		return runAsJob;
 	}
 
 	/**
@@ -810,11 +904,56 @@
 	}
 
 	/**
+	 * Creates or removes markers, uses the current project rule.
+	 * The tasks to do are maintained by markersQueue and executed in the submission order
+	 */
+	class ApiAnalysisMarkersJob extends WorkspaceJob {
+
+		public ApiAnalysisMarkersJob(Runnable task) {
+			super("Creating markers on " + currentproject.getName()); //$NON-NLS-1$
+			markersQueue.add(task);
+			setRule(currentproject);
+			setSystem(true);
+		}
+
+		@Override
+		public boolean belongsTo(Object family) {
+			return super.belongsTo(family) || ApiAnalysisMarkersJob.class == family;
+		}
+
+		@Override
+		public IStatus runInWorkspace(IProgressMonitor monitor) throws CoreException {
+			while (!markersQueue.isEmpty()) {
+				Runnable task = markersQueue.poll();
+				task.run();
+			}
+			return Status.OK_STATUS;
+		}
+
+	}
+
+	/**
 	 * Creates new markers are for the listing of problems added to this
 	 * reporter. If no problems have been added to this reporter, or we are not
 	 * running in the framework, no work is done.
 	 */
 	protected void createMarkers() {
+		IApiProblem[] problems = getAnalyzer().getProblems();
+		if (isRunningAsJob()) {
+			new ApiAnalysisMarkersJob(() -> createMarkersInternally(problems)).schedule();
+		} else {
+			createMarkersInternally(problems);
+		}
+	}
+
+	/**
+	 * Creates new markers are for the listing of problems added to this reporter.
+	 * If no problems have been added to this reporter, or we are not running in the
+	 * framework, no work is done.
+	 *
+	 * @param problems
+	 */
+	protected void createMarkersInternally(IApiProblem[] problems) {
 		try {
 			IResource manifest = Util.getManifestFile(this.currentproject);
 			if (manifest != null) {
@@ -825,7 +964,6 @@
 		} catch (CoreException e) {
 			ApiPlugin.log(e);
 		}
-		IApiProblem[] problems = getAnalyzer().getProblems();
 		String type = null;
 		for (IApiProblem problem : problems) {
 			int category = problem.getCategory();
diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/ICoreConstants.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/ICoreConstants.java
index a8db535..e28d771 100644
--- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/ICoreConstants.java
+++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/ICoreConstants.java
@@ -347,6 +347,11 @@
 	 */
 	String DISABLE_API_ANALYSIS_BUILDER = "Preferences.MainPage.disableAPIAnalysisBuilder";//$NON-NLS-1$
 	/**
+	 * Boolean preference whether API analysis should run asynchronous to the
+	 * build as background job
+	 */
+	String RUN_API_ANALYSIS_AS_JOB = "Preferences.MainPage.runAPIAnalysisAsJob";//$NON-NLS-1$
+	/**
 	 * Boolean preference whether add
 	 * '-Dorg.eclipse.swt.graphics.Resource.reportNonDisposed=true' to VM
 	 * arguments when creating a new launch configuration
diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/PreferenceInitializer.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/PreferenceInitializer.java
index 8f931fa..103ab6a 100644
--- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/PreferenceInitializer.java
+++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/PreferenceInitializer.java
@@ -75,6 +75,7 @@
 		PDEPreferencesManager corePrefs = PDECore.getDefault().getPreferencesManager();
 		corePrefs.setDefault(ICoreConstants.WORKSPACE_PLUGINS_OVERRIDE_TARGET, true);
 		corePrefs.setDefault(ICoreConstants.DISABLE_API_ANALYSIS_BUILDER, false);
+		corePrefs.setDefault(ICoreConstants.RUN_API_ANALYSIS_AS_JOB, false);
 		corePrefs.setDefault(ICoreConstants.ADD_SWT_NON_DISPOSAL_REPORTING, true);
 		corePrefs.setDefault(ICoreConstants.TEST_PLUGIN_PATTERN, ICoreConstants.TEST_PLUGIN_PATTERN_DEFAULTVALUE);
 	}
diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/PDEUIMessages.java b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/PDEUIMessages.java
index cf95eba..2a79503 100644
--- a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/PDEUIMessages.java
+++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/PDEUIMessages.java
@@ -561,6 +561,7 @@
 	public static String MainPreferencePage_WorkspacePluginsOverrideTarget;
 	public static String MainPreferencePage_WorkspacePluginsOverrideTargetTooltip;
 	public static String MainPreferencePage_DisableAPIAnalysisBuilder;
+	public static String MainPreferencePage_RunAPIAnalysisBuilderAsJob;
 
 	public static String MainPreferencePage_test_plugin_pattern_group;
 	public static String MainPreferencePage_test_plugin_pattern_label;
diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/pderesources.properties b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/pderesources.properties
index c8e5f9b..4eac9b7 100644
--- a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/pderesources.properties
+++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/pderesources.properties
@@ -376,6 +376,7 @@
 MainPreferencePage_updateStale=&Update stale manifest files prior to launching
 MainPreferencePage_WorkspacePluginsOverrideTarget=Workspace p&lug-ins override target platform plug-ins with the same id
 MainPreferencePage_DisableAPIAnalysisBuilder=&Disable API analysis builder
+MainPreferencePage_RunAPIAnalysisBuilderAsJob=&Run API analysis builder as job
 MainPreferencePage_WorkspacePluginsOverrideTargetTooltip=When disabled, all plug-in versions from workspace and target platform are being used.
 MainPreferencePage_test_plugin_pattern_group=Test plug-in detection:
 MainPreferencePage_test_plugin_pattern_description=Source folders in test plug-ins are marked to contain test sources.
diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/preferences/MainPreferencePage.java b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/preferences/MainPreferencePage.java
index 2a17ee0..81f5306 100644
--- a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/preferences/MainPreferencePage.java
+++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/preferences/MainPreferencePage.java
@@ -140,6 +140,7 @@
 	private Button fShowTargetStatus;
 	private Button fAlwaysPreferWorkspace;
 	private Button fDisableAPIAnalysisBuilder;
+	private Button fRunAPIAnalysisBuilderAsJob;
 	private Button fAddSwtNonDisposalReporting;
 
 	private Text fRuntimeWorkspaceLocation;
@@ -199,6 +200,11 @@
 		fDisableAPIAnalysisBuilder.setText(PDEUIMessages.MainPreferencePage_DisableAPIAnalysisBuilder);
 		fDisableAPIAnalysisBuilder.setSelection(store.getBoolean(IPreferenceConstants.DISABLE_API_ANALYSIS_BUILDER));
 
+		fRunAPIAnalysisBuilderAsJob = new Button(optionComp, SWT.CHECK);
+		fRunAPIAnalysisBuilderAsJob.setText(PDEUIMessages.MainPreferencePage_RunAPIAnalysisBuilderAsJob);
+		fRunAPIAnalysisBuilderAsJob.setSelection(
+				PDECore.getDefault().getPreferencesManager().getBoolean(ICoreConstants.RUN_API_ANALYSIS_AS_JOB));
+
 		fAddSwtNonDisposalReporting = new Button(optionComp, SWT.CHECK);
 		fAddSwtNonDisposalReporting.setText(PDEUIMessages.MainPreferencePage_AddSwtNonDisposedToVMArguments);
 		fAddSwtNonDisposalReporting
@@ -381,13 +387,20 @@
 
 		}
 
+		boolean runAPIAnalysisAsJob = fRunAPIAnalysisBuilderAsJob.getSelection();
+		if (PDECore.getDefault().getPreferencesManager()
+				.getBoolean(ICoreConstants.RUN_API_ANALYSIS_AS_JOB) != runAPIAnalysisAsJob) {
+			PDEPreferencesManager prefs = PDECore.getDefault().getPreferencesManager();
+			prefs.setValue(ICoreConstants.RUN_API_ANALYSIS_AS_JOB, runAPIAnalysisAsJob);
+		}
+
 		boolean addSwtNonDisposalReporting = fAddSwtNonDisposalReporting.getSelection();
 		if (store.getBoolean(IPreferenceConstants.ADD_SWT_NON_DISPOSAL_REPORTING) != addSwtNonDisposalReporting) {
 			store.setValue(IPreferenceConstants.ADD_SWT_NON_DISPOSAL_REPORTING, addSwtNonDisposalReporting);
 			PDEPreferencesManager prefs = PDECore.getDefault().getPreferencesManager();
 			prefs.setValue(ICoreConstants.ADD_SWT_NON_DISPOSAL_REPORTING, addSwtNonDisposalReporting);
 		}
-
+		PDECore.getDefault().getPreferencesManager().savePluginPreferences();
 		PDEPlugin.getDefault().getPreferenceManager().savePluginPreferences();
 
 		PDEPreferencesManager launchingStore = PDELaunchingPlugin.getDefault().getPreferenceManager();