Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
import static java.lang.String.format;
import static java.nio.file.Files.isRegularFile;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.joining;
import static org.eclipse.jdt.launching.JavaRuntime.computeUnresolvedRuntimeClasspath;

import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ResourcesPlugin;
Expand All @@ -34,21 +34,71 @@
import com.salesforce.bazel.eclipse.core.BazelCore;
import com.salesforce.bazel.eclipse.core.BazelCorePlugin;
import com.salesforce.bazel.eclipse.core.model.BazelProject;
import com.salesforce.bazel.eclipse.core.util.trace.StopWatch;

@SuppressWarnings("restriction")
public class BazelClasspathContainerRuntimeResolver
implements IRuntimeClasspathEntryResolver, IRuntimeClasspathEntryResolver2 {

/**
* Thre resolution context is an optimization. It captures state during the resolution of a container with the goal
* to improve efficiency and performance.
*/
private static final class ContainerResolutionContext {
/**
* the resolved classpath entries (in insertion order, no duplicates)
*/
private final LinkedHashSet<IRuntimeClasspathEntry> resolvedClasspath = new LinkedHashSet<>();
/**
* the current resolution stack deepness
*/
private int currentDepth = 0;
/**
* the set of already processed projects to avoid duplicate work
*/
private final Set<IProject> processedProjects = new HashSet<>();

public void add(IRuntimeClasspathEntry runtimeClasspathEntry) {
resolvedClasspath.add(runtimeClasspathEntry);
}

/**
* @param project
* the project being resolved
* @return <code>true</code> if the project was never processed before, <code>false</code> otherwise
*/
public boolean beginResolvingProject(IProject project) {
currentDepth++;
return processedProjects.add(project);
}

public void endResolvingProject(IProject project) {
if (currentDepth == 0) {
throw new IllegalStateException("Mismatched begin/end resolving project calls");
}

currentDepth--;
}

public IRuntimeClasspathEntry[] getResolvedClasspath() {
return resolvedClasspath.toArray(new IRuntimeClasspathEntry[resolvedClasspath.size()]);
}

public boolean isDoneProcessingProjects() {
return currentDepth == 0;
}
}

private static final Logger LOG = LoggerFactory.getLogger(BazelClasspathContainerRuntimeResolver.class);

/**
* Stack of currently resolving projects in container entries. Used to avoid cycles in project dependencies when
* resolving classpath container entries.
* The current thread's resolution context. Used to avoid cycles in project dependencies when resolving classpath
* container entries.
*
* @see org.eclipse.jdt.launching.JavaRuntime.fgProjects (for similar implementation)
*/
private static final ThreadLocal<LinkedHashSet<IProject>> stackOfResolvingProjects =
ThreadLocal.withInitial(LinkedHashSet::new);
private static final ThreadLocal<ContainerResolutionContext> currentThreadResolutionContet =
ThreadLocal.withInitial(ContainerResolutionContext::new);

private static String extractRealJarName(String jarName) {
// copied (and adapted) from BlazeJavaWorkspaceImporter
Expand All @@ -74,7 +124,7 @@ public boolean isVMInstallReference(IClasspathEntry entry) {
return false;
}

private void populateWithRealJar(Collection<IRuntimeClasspathEntry> resolvedClasspath, IClasspathEntry e) {
private void populateWithRealJar(ContainerResolutionContext resolutionContext, IClasspathEntry e) {
var jarPath = e.getPath();
var jarName = extractRealJarName(jarPath.lastSegment());
if (!jarName.equals(jarPath.lastSegment())) {
Expand Down Expand Up @@ -105,23 +155,36 @@ private void populateWithRealJar(Collection<IRuntimeClasspathEntry> resolvedClas
e = JavaCore.newLibraryEntry(realJarPath, e.getSourceAttachmentPath(), e.getSourceAttachmentRootPath());
}

resolvedClasspath.add(new RuntimeClasspathEntry(e));
resolutionContext.add(new RuntimeClasspathEntry(e));
}

/**
* Resolves a project classpath reference into all possible output folders and transitives and adds it to the
* resolved classpath.
*
* @param resolvedClasspath
* the resolved classpath
* @param sourceProject
* @param projectToResolve
* the project reference
* @param resolutionContext
* the resolution context
* @throws CoreException
* in case of problems
*/
private void populateWithResolvedProject(Collection<IRuntimeClasspathEntry> resolvedClasspath,
IProject sourceProject) throws CoreException {
var javaProject = JavaCore.create(sourceProject);
private void populateWithResolvedProject(IProject projectToResolve, ContainerResolutionContext resolutionContext)
throws CoreException {
var javaProject = JavaCore.create(projectToResolve);

// performance: use a saved container for Bazel projects if possible
// (discovered in https://github.com/eclipseguru/bazel-eclipse/issues/37)
if (populateWithSavedContainer(javaProject, resolutionContext)) {
return; // we are done
}

if (LOG.isDebugEnabled()) {
LOG.debug(
"No saved container found. Resolving project reference '{}' (thread '{}')",
projectToResolve.getName(),
Thread.currentThread().getName());
}

// never exclude test code because we use it for runtime dependencies as well
final var excludeTestCode = false;
Expand All @@ -131,49 +194,77 @@ private void populateWithResolvedProject(Collection<IRuntimeClasspathEntry> reso
for (IRuntimeClasspathEntry unresolvedEntry : unresolvedRuntimeClasspath) {
// resolve and add
stream(JavaRuntime.resolveRuntimeClasspathEntry(unresolvedEntry, javaProject, excludeTestCode))
.forEach(resolvedClasspath::add);
.forEach(resolutionContext::add);
}
}

private void populateWithSavedContainer(IJavaProject project,
LinkedHashSet<IRuntimeClasspathEntry> resolvedClasspath, Set<IProject> processedProjects)
/**
* Populates the resolved classpath with entries from the saved Bazel classpath container and ensures they are all
* fully resolved.
*
* @param project
* the project whose saved container should be used
* @param resolutionContext
* the resolution context
* @return <code>true</code> if a saved container was found and used, <code>false</code> otherwise
* @throws CoreException
*/
private boolean populateWithSavedContainer(IJavaProject project, ContainerResolutionContext resolutionContext)
throws CoreException {
var bazelContainer = getClasspathManager().getSavedContainer(project.getProject());
if (bazelContainer != null) {
var workspaceRoot = project.getResource().getWorkspace().getRoot();
var entries = bazelContainer.getFullClasspath();
for (IClasspathEntry e : entries) {
switch (e.getEntryKind()) {
case IClasspathEntry.CPE_PROJECT: {
// projects need to be resolved properly so we have all the output folders and exported jars on the classpath
var sourceProject = workspaceRoot.getProject(e.getPath().segment(0));
if (processedProjects.add(sourceProject)) {
if (bazelContainer == null) {
// nothing available
LOG.debug("No saved Bazel classpath container found for project '{}'", project.getProject().getName());
return false;
}

if (LOG.isDebugEnabled()) {
LOG.debug(
"Populating classpath with saved container of project '{}' (thread '{}')",
project.getProject().getName(),
Thread.currentThread().getName());
}

var workspaceRoot = project.getResource().getWorkspace().getRoot();
var entries = bazelContainer.getFullClasspath();
for (IClasspathEntry e : entries) {
switch (e.getEntryKind()) {
case IClasspathEntry.CPE_PROJECT: {
// projects need to be resolved properly so we have all the output folders and exported jars on the classpath
var sourceProject = workspaceRoot.getProject(e.getPath().segment(0));
if (resolutionContext.beginResolvingProject(sourceProject)) {
try {
// only resolve and add the projects if it was never attempted before
populateWithResolvedProject(resolvedClasspath, sourceProject);
} else if (LOG.isDebugEnabled()) {
LOG.debug(
"Skipping recursive resolution attempt for project '{}' in thread '{}' ({})",
sourceProject,
Thread.currentThread().getName(),
processedProjects.stream().map(IProject::getName).collect(joining(" > ")));
populateWithResolvedProject(sourceProject, resolutionContext);
} finally {
// remove from stack again when done resolving
resolutionContext.endResolvingProject(sourceProject);
}
break;
} else if (LOG.isDebugEnabled()) {
// this should not happen in theory because Bazel is an acyclic graph as well as Eclipse doesn't like it but who knows...
LOG.debug(
"Skipping recursive resolution attempt for project '{}' in thread '{}' ({})",
sourceProject,
Thread.currentThread().getName());
}
case IClasspathEntry.CPE_LIBRARY: {
// we can rely on the assumption that this is an absolute path pointing into Bazel's execroot
// but we have to exclude ijars from runtime
populateWithRealJar(resolvedClasspath, e);
break;
}
default:
throw new CoreException(
Status.error(
format(
"Unexpected classpath entry in the persisted Bazel container. Try refreshing the classpath or report as bug. %s",
e)));
break;
}
case IClasspathEntry.CPE_LIBRARY: {
// we can rely on the assumption that this is an absolute path pointing into Bazel's execroot
// but we have to exclude ijars from runtime
populateWithRealJar(resolutionContext, e);
break;
}
default:
throw new CoreException(
Status.error(
format(
"Unexpected classpath entry in the persisted Bazel container. Try refreshing the classpath or report as bug. %s",
e)));
}
}

return true;
}

@Override
Expand All @@ -188,37 +279,68 @@ public IRuntimeClasspathEntry[] resolveRuntimeClasspathEntry(IRuntimeClasspathEn
return new IRuntimeClasspathEntry[0];
}

if (LOG.isDebugEnabled()) {
LOG.debug(
"Classpath entry resolution request in thread '{}' for Bazel container in project '{}'",
Thread.currentThread().getName(),
project.getProject().getName());
}

// this method can be entered recursively; luckily only within the same thread
// therefore we use a ThreadLocal LinkedHashSet to keep track of recursive attempts
var currentlyResolvingProjects = stackOfResolvingProjects.get();
currentlyResolvingProjects.add(project.getProject());
try {
var result = new LinkedHashSet<IRuntimeClasspathEntry>(); // insertion order is important but avoid duplicates
var resolutionContext = currentThreadResolutionContet.get();
if (!resolutionContext.beginResolvingProject(project.getProject())) {
LOG.warn(
"Detected recursive resolution attempt for project '{}' in thread '{}' ({})",
project.getProject().getName(),
Thread.currentThread().getName());
return new IRuntimeClasspathEntry[0];
}

var isTopLevelResolution = resolutionContext.currentDepth == 0;
var stopWatch = StopWatch.startNewStopWatch();
try {
// try the saved container
// this is usually ok because we no longer use the ijars on project classpaths
// the saved container also contains all runtime dependencies by default
populateWithSavedContainer(project, result, currentlyResolvingProjects);
populateWithSavedContainer(project, resolutionContext);

var bazelProject = BazelCore.create(project.getProject());
if (bazelProject.isWorkspaceProject()) {
// when debugging the workspace project we include all target/package projects automatically
// when debugging/launching in the workspace project we include all target/package projects automatically
// this is odd because the projects should cause cyclic dependencies
// however it is convenient with source code lookups for missing dependencies
var bazelProjects = bazelProject.getBazelWorkspace().getBazelProjects();
for (BazelProject sourceProject : bazelProjects) {
if (!sourceProject.isWorkspaceProject()) {
populateWithResolvedProject(result, sourceProject.getProject());
populateWithResolvedProject(sourceProject.getProject(), resolutionContext);
}
}
}

return result.toArray(new IRuntimeClasspathEntry[result.size()]);

return resolutionContext.getResolvedClasspath();
} finally {
currentlyResolvingProjects.remove(project.getProject());
if (currentlyResolvingProjects.isEmpty()) {
stackOfResolvingProjects.remove();
resolutionContext.endResolvingProject(project.getProject());

if (isTopLevelResolution) {
// clean up thread local when we are done with the top-level resolution
currentThreadResolutionContet.remove();

if (!resolutionContext.isDoneProcessingProjects()) {
LOG.error(
"Resolution context for thread '{}' is not fully ended after top-level resolution of project '{}' was done. There are missing start/end calls. Please report as bug.",
Thread.currentThread().getName(),
project.getProject().getName());
}
}

stopWatch.stop();

if (LOG.isDebugEnabled()) {
LOG.debug(
"Resolved Bazel classpath container for project '{}' in {}ms",
project.getProject().getName(),
stopWatch.getDuration(TimeUnit.MILLISECONDS));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ public interface StopCallback {
void stopped(StopWatch stopWatch);
}

public static StopWatch startNewStopWatch() {
return new StopWatch().start();
}

private final StopCallback stopCallback;
private volatile long startTimeNanos;
private volatile long endTimeNanos;
Expand Down Expand Up @@ -75,9 +79,12 @@ public long getDuration(final TimeUnit timeUnit) {

/**
* Sets the start time to the current value of {@link System#nanoTime()}.
*
* @return this stop watch instance for convenience
*/
public void start() {
public StopWatch start() {
startTimeNanos = System.nanoTime();
return this;
}

/**
Expand Down
Loading