/*
* Copyright (c) 2013, the Dart project authors.
*
* Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.eclipse.org/legal/epl-v10.html
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.dart.tools.core.internal.builder;
import com.google.dart.engine.ast.CompilationUnit;
import com.google.dart.engine.context.AnalysisContext;
import com.google.dart.engine.context.AnalysisResult;
import com.google.dart.engine.context.ChangeNotice;
import com.google.dart.engine.error.AnalysisError;
import com.google.dart.engine.html.ast.HtmlUnit;
import com.google.dart.engine.internal.context.AnalysisOptionsImpl;
import com.google.dart.engine.sdk.DartSdk;
import com.google.dart.engine.source.Source;
import com.google.dart.engine.utilities.source.LineInfo;
import com.google.dart.tools.core.DartCore;
import com.google.dart.tools.core.analysis.model.AnalysisEvent;
import com.google.dart.tools.core.analysis.model.AnalysisListener;
import com.google.dart.tools.core.analysis.model.ContextManager;
import com.google.dart.tools.core.analysis.model.Project;
import com.google.dart.tools.core.analysis.model.ProjectManager;
import com.google.dart.tools.core.analysis.model.ResolvedEvent;
import com.google.dart.tools.core.analysis.model.ResolvedHtmlEvent;
import com.google.dart.tools.core.analysis.model.ResourceMap;
import com.google.dart.tools.core.model.DartSdkManager;
import com.google.dart.tools.core.utilities.io.PrintStringWriter;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.IPath;
/**
* Instances of {@code AnalysisWorker} perform analysis by repeatedly calling
* {@link AnalysisContext#performAnalysisTask()} and update both the index and the error markers
* based upon the analysis results.
*
* @coverage dart.tools.core.builder
*/
public class AnalysisWorker {
/**
* Internal implementation of events broadcast by the worker.
*/
private abstract class AbstractEvent implements AnalysisEvent {
private final AnalysisContext context;
private final ResourceMap resourceMap;
Source source;
IResource resource;
public AbstractEvent(AnalysisContext context, ResourceMap resourceMap) {
this.context = context;
this.resourceMap = resourceMap;
}
@Override
public AnalysisContext getContext() {
return context;
}
@Override
public ContextManager getContextManager() {
return contextManager;
}
@Override
public ResourceMap getResourceMap() {
return resourceMap;
}
}
/**
* Internal implementation of events broadcast by the worker.
*/
private class Event extends AbstractEvent implements ResolvedEvent {
CompilationUnit unit;
public Event(AnalysisContext context, ResourceMap resourceMap) {
super(context, resourceMap);
}
@Override
public IResource getResource() {
return resource;
}
@Override
public Source getSource() {
return source;
}
@Override
public CompilationUnit getUnit() {
return unit;
}
}
/**
* Internal implementation of events broadcast by the worker.
*/
private class HtmlEvent extends AbstractEvent implements ResolvedHtmlEvent {
HtmlUnit unit;
public HtmlEvent(AnalysisContext context, ResourceMap resourceMap) {
super(context, resourceMap);
}
@Override
public IResource getResource() {
return resource;
}
@Override
public Source getSource() {
return source;
}
@Override
public HtmlUnit getUnit() {
return unit;
}
}
/**
* The {@link AnalysisContext} cache size for a context when background analysis is not being
* performed on that context.
*/
private static final int IDLE_CACHE_SIZE = AnalysisOptionsImpl.DEFAULT_CACHE_SIZE;
/**
* The {@link AnalysisContext} cache size for a context when background analysis is being
* performed on that context.
*/
private static int WORKING_CACHE_SIZE = computeWorkingCacheSize();
private static final int WORKING_CACHE_SIZE_DEFAULT = IDLE_CACHE_SIZE * 2;
private static final int WORKING_CACHE_256_MEMORY = 450 * 1024 * 1024;
private static final int WORKING_CACHE_256_SIZE = 256;
private static final int WORKING_CACHE_512_MEMORY = 900 * 1024 * 1024;
private static final int WORKING_CACHE_512_SIZE = 512;
/**
* Objects to be notified when each compilation unit has been resolved. Contents of this array
* will not change, but the array itself may be replaced. Synchronize against
* {@link #allListenersLock} before accessing this field.
*/
private static AnalysisListener[] allListeners = new AnalysisListener[] {};
/**
* Synchronize against {@code #allListenersLock} before accessing {@link #allListeners}
*/
private static final Object allListenersLock = new Object();
/**
* Add a listener to be notified when compilation units are resolved
*
* @param listener the listener
*/
public static void addListener(AnalysisListener listener) {
if (listener == null) {
return;
}
synchronized (allListenersLock) {
for (AnalysisListener each : allListeners) {
if (listener == each) {
return;
}
}
int oldLen = allListeners.length;
AnalysisListener[] newListeners = new AnalysisListener[oldLen + 1];
System.arraycopy(allListeners, 0, newListeners, 0, oldLen);
newListeners[oldLen] = listener;
allListeners = newListeners;
}
}
/**
* Ensure that a worker is at the front of the queue to update the analysis for the context.
*
* @param manager the manager containing the context to be analyzed (not {@code null})
* @param context the context to be analyzed (not {@code null})
* @see #performAnalysis(AnalysisManager)
*/
public static void performAnalysisInBackground(ContextManager manager, AnalysisContext context) {
AnalysisManager.getInstance().performAnalysisInBackground(manager, context);
}
/**
* Remove a listener from the list of objects to be notified.
*
* @param listener the listener to be removed
*/
public static void removeListener(AnalysisListener listener) {
synchronized (allListenersLock) {
for (int index = 0; index < allListeners.length; index++) {
if (listener == allListeners[index]) {
int oldLen = allListeners.length;
AnalysisListener[] newListeners = new AnalysisListener[oldLen - 1];
System.arraycopy(allListeners, 0, newListeners, 0, index);
System.arraycopy(allListeners, index + 1, newListeners, index, oldLen - index - 1);
allListeners = newListeners;
return;
}
}
}
}
/**
* Wait for any scheduled background analysis to complete or for the specified duration to elapse.
*
* @param milliseconds the number of milliseconds to wait
* @return {@code true} if the background analysis has completed, else {@code false}
*/
public static boolean waitForBackgroundAnalysis(long milliseconds) {
return AnalysisManager.getInstance().waitForBackgroundAnalysis(milliseconds);
}
/**
* Returns what cache size to use for working {@link AnalysisContext}, currently depending on
* maximum heap size.
*/
private static int computeWorkingCacheSize() {
long maxMemory = Runtime.getRuntime().maxMemory();
if (maxMemory > WORKING_CACHE_512_MEMORY) {
return WORKING_CACHE_512_SIZE;
} else if (maxMemory > WORKING_CACHE_256_MEMORY) {
return WORKING_CACHE_256_SIZE;
}
return WORKING_CACHE_SIZE_DEFAULT;
}
/**
* The context manager containing the source for this context (not {@code null}).
*/
protected final ContextManager contextManager;
/**
* An object used to synchronously access the {@link #context} field.
*/
private final Object lock = new Object();
/**
* The analysis context on which analysis is performed or {@code null} if either the analysis is
* stopped or complete. Synchronize against {@link #lock} before accessing this field.
*/
private AnalysisContext context;
/**
* The marker manager used to translate errors into Eclipse markers (not {@code null}).
*/
private final AnalysisMarkerManager markerManager;
/**
* The project manager used to obtain the index to be updated and used to notify others when
* analysis is complete (not {@code null}).
*/
private final ProjectManager projectManager;
/**
* Contains information about the compilation unit that was resolved.
*/
private final Event event;
/**
* Contains information about the HTML unit that was resolved.
*/
private final HtmlEvent htmlEvent;
/**
* Flag to prevent log from being saturated with exceptions.
*/
private static boolean exceptionLogged = false;
/**
* Construct a new instance for performing analysis which updates the
* {@link ProjectManager#getIndex() default index} and uses the
* {@link AnalysisMarkerManager#getInstance() default marker manager} to translate errors into
* Eclipse markers.
*
* @param manager the manager containing sources for the specified context (not {@code null})
* @param context the context used to perform the analysis (not {@code null})
*/
public AnalysisWorker(ContextManager manager, AnalysisContext context) {
this(
manager,
context,
DartCore.getProjectManager(),
DartCore.getProjectManager().getResourceMap(context),
AnalysisMarkerManager.getInstance());
}
/**
* Construct a new instance for performing analysis.
*
* @param contextManager manager containing sources for the specified context (not {@code null})
* @param context the context used to perform the analysis (not {@code null})
* @param projectManager used to obtain the index to be updated and notified others when analysis
* is complete (not {@code null})
* @param resourceMap the resource map for the given context
* @param markerManager used to translate errors into Eclipse markers (not {@code null})
*/
public AnalysisWorker(ContextManager contextManager, AnalysisContext context,
ProjectManager projectManager, ResourceMap resourceMap, AnalysisMarkerManager markerManager) {
this.contextManager = contextManager;
this.context = context;
this.projectManager = projectManager;
this.markerManager = markerManager;
this.contextManager.addWorker(this);
this.event = new Event(context, resourceMap);
this.htmlEvent = new HtmlEvent(context, resourceMap);
}
/**
* Answer the context being processed by the receiver.
*
* @return the context or {@code null} if processing has been stopped or is complete
*/
public AnalysisContext getContext() {
synchronized (lock) {
return context;
}
}
/**
* Perform analysis by repeatedly calling {@link AnalysisContext#performAnalysisTask()} and update
* both the index and the error markers based upon the analysis results.
*
* @param manager the {@link AnalysisManager} or {@code null} if is performed without a manager
*/
public void performAnalysis(AnalysisManager manager) {
// Check if project exists
if (contextManager == null || contextManager.getResource() == null
|| !contextManager.getResource().exists()) {
return;
}
// Check for a valid context and SDK
DartSdk sdk;
synchronized (lock) {
if (context == null) {
return;
}
sdk = context.getSourceFactory().getDartSdk();
}
boolean hasSdk = sdk != DartSdkManager.NONE;
markerManager.queueHasDartSdk(contextManager.getResource(), hasSdk);
if (!hasSdk) {
return;
}
// Check if the context has been set to null indicating that analysis should stop
AnalysisContext context;
synchronized (lock) {
if (this.context == null) {
return;
}
context = this.context;
}
setCacheSize(context, WORKING_CACHE_SIZE);
boolean analysisComplete = false;
while (true) {
// Check if the context has been set to null indicating that analysis should stop
synchronized (lock) {
if (this.context == null) {
break;
}
}
// Exit if no more analysis to be performed (changes == null)
AnalysisResult result;
try {
result = context.performAnalysisTask();
} catch (RuntimeException e) {
DartCore.logError("Analysis Failed: " + contextManager, e);
break;
}
ChangeNotice[] changes = result.getChangeNotices();
if (changes == null) {
analysisComplete = true;
break;
}
// Process changes and allow subclasses to check results
processChanges(context, changes);
checkResults(context);
}
setCacheSize(context, IDLE_CACHE_SIZE);
stop();
markerManager.done();
// Notify others that analysis is complete
if (analysisComplete) {
notifyComplete();
}
}
/**
* Queue this worker to have {@link #performAnalysis(AnalysisManager)} called in a background job.
*
* @see #performAnalysisInBackground(Project, AnalysisContext)
*/
public void performAnalysisInBackground() {
AnalysisManager.getInstance().addWorker(this);
}
/**
* Signal the receiver to stop analysis.
*/
public void stop() {
synchronized (lock) {
context = null;
}
contextManager.removeWorker(this);
}
/**
* Subclasses may override this method to call various "get" methods on the context looking to see
* if information it needs is cached.
*
* @param context the analysis context being processed (not {@code null})
*/
protected void checkResults(AnalysisContext context) {
}
/**
* Notify those interested that the analysis is complete.
*/
private void notifyComplete() {
if (contextManager instanceof Project) {
projectManager.projectAnalyzed((Project) contextManager);
}
AnalysisListener[] currentListeners;
synchronized (allListenersLock) {
currentListeners = allListeners;
}
for (AnalysisListener listener : currentListeners) {
try {
listener.complete(event);
} catch (Exception e) {
if (!exceptionLogged) {
// Log at most one exception so as not to flood the log
exceptionLogged = true;
DartCore.logError("Exception notifying listener that analysis is complete", e);
}
}
}
}
/**
* Notify those interested that a compilation unit has been resolved.
*
* @param context the analysis context containing the unit that was resolved (not {@code null})
* @param unit the unit that was resolved (not {@code null})
* @param source the source of the unit that was resolved (not {@code null})
* @param resource the resource of the unit that was resolved or {@code null} if outside the
* workspace
*/
private void notifyResolved(AnalysisContext context, CompilationUnit unit, Source source,
IResource resource) {
AnalysisListener[] currentListeners;
synchronized (allListenersLock) {
currentListeners = allListeners;
}
event.unit = unit;
event.source = source;
event.resource = resource;
for (AnalysisListener listener : currentListeners) {
try {
listener.resolved(event);
} catch (Exception e) {
if (!exceptionLogged) {
// Log at most one exception so as not to flood the log
exceptionLogged = true;
DartCore.logError("Exception notifying listener of resolved unit: " + source, e);
}
}
}
}
/**
* Notify those interested that a HTML unit has been resolved.
*
* @param context the analysis context containing the unit that was resolved (not {@code null})
* @param unit the unit that was resolved (not {@code null})
* @param source the source of the unit that was resolved (not {@code null})
* @param resource the resource of the unit that was resolved or {@code null} if outside the
* workspace
*/
private void notifyResolved(AnalysisContext context, HtmlUnit unit, Source source,
IResource resource) {
AnalysisListener[] currentListeners;
synchronized (allListenersLock) {
currentListeners = allListeners;
}
htmlEvent.unit = unit;
htmlEvent.source = source;
htmlEvent.resource = resource;
for (AnalysisListener listener : currentListeners) {
try {
listener.resolvedHtml(htmlEvent);
} catch (Exception e) {
if (!exceptionLogged) {
// Log at most one exception so as not to flood the log
exceptionLogged = true;
DartCore.logError("Exception notifying listener of resolved HTML unit: " + source, e);
}
}
}
}
/**
* Update both the index and the error markers based upon the analysis results.
*
* @param context the analysis context containing the unit that was resolved (not {@code null})
* @param changes the changes to be processed (not {@code null})
*/
private void processChanges(AnalysisContext context, ChangeNotice[] changes) {
for (ChangeNotice change : changes) {
Source source = change.getSource();
IResource res = contextManager.getResource(source);
// If errors are available, then queue the errors to be translated to markers
AnalysisError[] errors = change.getErrors();
if (errors != null) {
if (res == null) {
// TODO (danrubel): log unmatched sources once context
// only returns errors for added sources
// DartCore.logError("Failed to determine resource for: " + source);
} else {
IPath location = res.getLocation();
if (location != null && !DartCore.isContainedInPackages(location.toFile())) {
LineInfo lineInfo = change.getLineInfo();
if (lineInfo == null) {
// Sometimes this happens in UI tests, but we don't know what error.
@SuppressWarnings("resource")
PrintStringWriter writer = new PrintStringWriter();
writer.print("Missing line information for: ");
writer.println(source);
for (AnalysisError error : errors) {
writer.print("Error: ");
writer.print(error.getErrorCode());
writer.print(" ");
writer.print(error.getSource());
writer.print(" ");
writer.print(error.getOffset());
writer.print(" ");
writer.print(error.getLength());
writer.print(" ");
writer.println(error.getMessage());
}
DartCore.logInformation(writer.toString());
} else {
markerManager.queueErrors(res, lineInfo, errors);
}
}
}
}
// If there is a resolved unit, then then notify others such as indexer
CompilationUnit unit = change.getCompilationUnit();
if (unit != null) {
notifyResolved(context, unit, source, res);
}
// If there is a resolved HTML unit, then then notify others such as indexer
HtmlUnit htmlUnit = change.getHtmlUnit();
if (htmlUnit != null) {
notifyResolved(context, htmlUnit, source, res);
}
}
}
private void setCacheSize(AnalysisContext context, int cacheSize) {
AnalysisOptionsImpl options = new AnalysisOptionsImpl(context.getAnalysisOptions());
options.setCacheSize(cacheSize);
context.setAnalysisOptions(options);
}
}