Refactor of multithreading compiler. This splits the logic out into new classes to get it out of the main GWTCompiler class. It also uses java.util.concurrency instead of primitive locks, so it should be a bit easier to determine what's going on.
Lex's reservations / TODO items:
- The OOM memory handling is still pretty complicated, it feels like there must be a more clear way to organize this to be sure we're hitting all the cases correctly.
- The heuristic really needs more work to better estimate actual memory usage.
Review by: spoon (see notes)
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@3349 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/GWTCompiler.java b/dev/core/src/com/google/gwt/dev/GWTCompiler.java
index 3fe8068..0a38598 100644
--- a/dev/core/src/com/google/gwt/dev/GWTCompiler.java
+++ b/dev/core/src/com/google/gwt/dev/GWTCompiler.java
@@ -19,12 +19,9 @@
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.TreeLogger.Type;
import com.google.gwt.core.ext.linker.ArtifactSet;
-import com.google.gwt.core.ext.linker.SelectionProperty;
-import com.google.gwt.core.ext.linker.impl.StandardCompilationResult;
import com.google.gwt.core.ext.linker.impl.StandardLinkerContext;
import com.google.gwt.dev.cfg.ModuleDef;
import com.google.gwt.dev.cfg.ModuleDefLoader;
-import com.google.gwt.dev.cfg.Properties;
import com.google.gwt.dev.cfg.Property;
import com.google.gwt.dev.cfg.PropertyPermutations;
import com.google.gwt.dev.cfg.Rules;
@@ -36,7 +33,6 @@
import com.google.gwt.dev.jjs.JJSOptions;
import com.google.gwt.dev.jjs.JavaToJavaScriptCompiler;
import com.google.gwt.dev.jjs.JsOutputOption;
-import com.google.gwt.dev.jjs.ast.JProgram;
import com.google.gwt.dev.shell.StandardRebindOracle;
import com.google.gwt.dev.util.PerfLogger;
import com.google.gwt.dev.util.Util;
@@ -194,197 +190,6 @@
}
}
- /**
- * Represents the state of a single permutation for compile.
- */
- private static final class Permuation {
- final TreeLogger logger;
- final int number;
- final CompilationRebindOracle rebindOracle;
-
- public Permuation(TreeLogger logger, int number,
- CompilationRebindOracle rebindOracle) {
- this.logger = logger;
- this.number = number;
- this.rebindOracle = rebindOracle;
- }
- }
-
- /**
- * Contains all shared state and synchronizes among permutation workers and
- * the main thread.
- *
- * TODO: clean this up using java.util.concurrent
- */
- private static final class PermutationManager {
-
- private boolean failed;
- private final StandardLinkerContext linkerContext;
- private final TreeLogger logger;
- private int nextPerm = 0;
- private int numThreads;
- private final CompilationRebindOracle[] rebindOracles;
-
- public PermutationManager(TreeLogger logger,
- StandardLinkerContext linkerContext,
- DistillerRebindPermutationOracle rpo, int numThreads) {
- this.logger = logger;
- this.linkerContext = linkerContext;
- this.numThreads = numThreads;
- rebindOracles = new CompilationRebindOracle[rpo.getPermuationCount()];
- for (int i = 0; i < rebindOracles.length; ++i) {
- rebindOracles[i] = rpo.getRebindOracle(i);
- }
- }
-
- public synchronized void finishPermuation(Permuation perm, String js) {
- StandardCompilationResult compilation;
- try {
- compilation = linkerContext.getCompilation(perm.logger, js);
- } catch (UnableToCompleteException e) {
- recordFailure();
- return;
- }
- StaticPropertyOracle propOracle = perm.rebindOracle.getPropertyOracle();
- Property[] orderedProps = propOracle.getOrderedProps();
- String[] orderedPropValues = propOracle.getOrderedPropValues();
- Map<SelectionProperty, String> unboundProperties = new HashMap<SelectionProperty, String>();
- for (int i = 0; i < orderedProps.length; i++) {
- SelectionProperty key = linkerContext.getProperty(orderedProps[i].getName());
- if (key.tryGetValue() != null) {
- /*
- * The view of the Permutation doesn't include properties with defined
- * values.
- */
- continue;
- }
- unboundProperties.put(key, orderedPropValues[i]);
- }
- compilation.addSelectionPermutation(unboundProperties);
- }
-
- public synchronized int getActiveThreadCount() {
- return numThreads;
- }
-
- public synchronized Permuation getNextPermuation() {
- if (hasNextPermutation()) {
- // Make sure we have enough memory.
- int permNumber = nextPerm++;
- while (nextPerm < rebindOracles.length
- && rebindOracles[nextPerm] == null) {
- ++nextPerm;
- }
- TreeLogger branch = logger.branch(TreeLogger.TRACE,
- "Analyzing permutation #" + (permNumber + 1), null);
- return new Permuation(branch, permNumber, rebindOracles[permNumber]);
- }
- return null;
- }
-
- public synchronized boolean isFailed() {
- return failed;
- }
-
- public synchronized void outOfMemory(Permuation perm, OutOfMemoryError e) {
- if (getActiveThreadCount() > 1) {
- // Recycle and try on a different thread.
- perm.logger.log(TreeLogger.WARN,
- "Out of memory; will retry permutation using fewer concurrent threads");
- rebindOracles[perm.number] = perm.rebindOracle;
- if (nextPerm > perm.number) {
- nextPerm = perm.number;
- }
- } else {
- // Only one thread, we're truly out of memory!
- perm.logger.log(TreeLogger.ERROR, null, e);
- }
- }
-
- @SuppressWarnings("unused")
- public synchronized void recordFailure() {
- failed = true;
- }
-
- public synchronized void threadDied() {
- --numThreads;
- }
-
- private boolean hasNextPermutation() {
- return !isFailed() && nextPerm < rebindOracles.length;
- }
- }
-
- /**
- * A worker thread for compiling individual permutations.
- */
- private static final class PermutationWorker implements Runnable {
- private static void logProperties(TreeLogger logger,
- StaticPropertyOracle propOracle) {
- Property[] props = propOracle.getOrderedProps();
- String[] values = propOracle.getOrderedPropValues();
- if (logger.isLoggable(TreeLogger.DEBUG)) {
- logger = logger.branch(TreeLogger.DEBUG, "Setting properties", null);
- for (int i = 0; i < props.length; i++) {
- String name = props[i].getName();
- String value = values[i];
- logger.log(TreeLogger.TRACE, name + " = " + value, null);
- }
- }
- }
-
- private final JavaToJavaScriptCompiler jjs;
- private final PermutationManager manager;
-
- public PermutationWorker(PermutationManager localState,
- JavaToJavaScriptCompiler jjs) {
- this.manager = localState;
- this.jjs = jjs;
- }
-
- public void run() {
- try {
- while (hasEnoughMemory()) {
- Permuation perm = manager.getNextPermuation();
- if (perm == null) {
- return;
- }
- PerfLogger.start("Permutation #" + (perm.number + 1));
- try {
- logProperties(perm.logger, perm.rebindOracle.getPropertyOracle());
- String js = jjs.compile(perm.logger, perm.rebindOracle);
- manager.finishPermuation(perm, js);
- // Allow GC.
- js = null;
- } catch (OutOfMemoryError e) {
- manager.outOfMemory(perm, e);
- return;
- } catch (Throwable e) {
- perm.logger.log(TreeLogger.ERROR, "Permutation failed", e);
- manager.recordFailure();
- return;
- } finally {
- PerfLogger.end();
- }
- }
- } finally {
- manager.threadDied();
- }
- }
-
- private boolean hasEnoughMemory() {
- if (manager.getActiveThreadCount() == 1) {
- // I'm the last thread, so it doesn't matter, we have to try.
- return true;
- }
- if (jjs.getAstMemoryUsage() < getPotentialFreeMemory()) {
- // Best effort memory reclaim.
- System.gc();
- }
- return jjs.getAstMemoryUsage() < getPotentialFreeMemory();
- }
- }
-
public static final String GWT_COMPILER_DIR = ".gwt-tmp" + File.separatorChar
+ "compiler";
@@ -406,19 +211,8 @@
System.exit(1);
}
- private static long getPotentialFreeMemory() {
- long used = Runtime.getRuntime().totalMemory()
- - Runtime.getRuntime().freeMemory();
- assert (used > 0);
- long potentialFree = Runtime.getRuntime().maxMemory() - used;
- assert (potentialFree >= 0);
- return potentialFree;
- }
-
private CompilationState compilationState;
- private String[] declEntryPts;
-
private File genDir;
private File generatorResourcesDir;
@@ -433,12 +227,6 @@
private File outDir;
- private PropertyPermutations perms;
-
- private Properties properties;
-
- private DistillerRebindPermutationOracle rebindPermOracle;
-
private Rules rules;
private boolean useGuiLogger;
@@ -516,6 +304,7 @@
compilationState.compile(logger);
rules = module.getRules();
+ String[] declEntryPts;
if (jjsOptions.isValidateOnly()) {
// TODO: revisit this.. do we even need to run JJS?
logger.log(TreeLogger.INFO, "Validating compilation " + module.getName(),
@@ -534,13 +323,11 @@
}
ArtifactSet generatorArtifacts = new ArtifactSet();
- properties = module.getProperties();
- perms = new PropertyPermutations(properties);
- rebindPermOracle = new DistillerRebindPermutationOracle(generatorArtifacts,
- perms);
+ DistillerRebindPermutationOracle rpo = new DistillerRebindPermutationOracle(
+ generatorArtifacts, new PropertyPermutations(module.getProperties()));
WebModeCompilerFrontEnd frontEnd = new WebModeCompilerFrontEnd(
- compilationState, rebindPermOracle);
+ compilationState, rpo);
JavaToJavaScriptCompiler jjs = new JavaToJavaScriptCompiler(logger,
frontEnd, declEntryPts, jjsOptions);
@@ -551,7 +338,7 @@
StandardLinkerContext linkerContext = new StandardLinkerContext(logger,
module, outDir, generatorResourcesDir, jjsOptions);
- compilePermutations(logger, jjs, linkerContext);
+ compilePermutations(logger, jjs, rpo, linkerContext);
logger.log(TreeLogger.INFO, "Compilation succeeded", null);
linkerContext.addOrReplaceArtifacts(generatorArtifacts);
@@ -622,100 +409,20 @@
}
private void compilePermutations(TreeLogger logger,
- JavaToJavaScriptCompiler jjs, final StandardLinkerContext linkerContext)
- throws UnableToCompleteException {
- PerfLogger.start("Compiling " + perms.size() + " permutations");
+ JavaToJavaScriptCompiler jjs, DistillerRebindPermutationOracle rpo,
+ StandardLinkerContext linkerContext) throws UnableToCompleteException {
- int threadCount = computeThreadCount(logger, jjs);
-
- PermutationManager manager = new PermutationManager(logger.branch(
- TreeLogger.DEBUG, "Compiling permutations", null), linkerContext,
- rebindPermOracle, threadCount);
-
- Thread[] threads = new Thread[threadCount];
- for (int i = 0; i < threadCount; ++i) {
- threads[i] = new Thread(new PermutationWorker(manager, jjs));
+ int permCount = rpo.getPermuationCount();
+ PerfLogger.start("Compiling " + permCount + " permutations");
+ Permutation[] perms = new Permutation[permCount];
+ for (int i = 0; i < permCount; ++i) {
+ CompilationRebindOracle rebindOracle = rpo.getRebindOracle(i);
+ perms[i] = new Permutation(i, rebindOracle,
+ rebindOracle.getPropertyOracle());
}
-
- for (Thread thread : threads) {
- thread.start();
- }
-
- for (Thread thread : threads) {
- try {
- thread.join();
- } catch (InterruptedException e) {
- throw new RuntimeException("Unexpected interruption");
- }
- }
-
- PerfLogger.end();
-
- if (manager.failed) {
- throw new UnableToCompleteException();
- }
- }
-
- private int computeThreadCount(TreeLogger logger, JavaToJavaScriptCompiler jjs) {
- /*
- * Don't need more threads than the number of permutations.
- */
- int result = rebindPermOracle.getPermuationCount();
-
- /*
- * Computation is mostly CPU bound, so don't use more threads than
- * processors.
- */
- result = Math.min(Runtime.getRuntime().availableProcessors(), result);
-
- /*
- * Allow user-defined override as an escape valve.
- */
- result = Math.min(result, Integer.getInteger("gwt.jjs.maxThreads", result));
-
- if (result == 1) {
- return 1;
- }
-
- // More than one thread would definitely be faster at this point.
-
- if (JProgram.isTracingEnabled()) {
- logger.log(TreeLogger.INFO,
- "Parallel compilation disabled due to gwt.jjs.traceMethods being enabled");
- return 1;
- }
-
- int desiredThreads = result;
-
- /*
- * Need to do some memory estimation to figure out how many concurrent
- * threads we can safely run.
- */
- long potentialFreeMemory = getPotentialFreeMemory();
- long astMemoryUsage = jjs.getAstMemoryUsage();
- int memUsageThreads = (int) (potentialFreeMemory / astMemoryUsage) + 1;
- logger.log(TreeLogger.TRACE,
- "Extra threads constrained by estimated memory usage: "
- + memUsageThreads + " = " + potentialFreeMemory + " / "
- + astMemoryUsage);
-
- if (memUsageThreads < desiredThreads) {
- long currentMaxMemory = Runtime.getRuntime().maxMemory();
- // Convert to megabytes.
- currentMaxMemory /= 1024 * 1024;
-
- long suggestedMaxMemory = currentMaxMemory * 2;
-
- logger.log(TreeLogger.WARN, desiredThreads
- + " threads could be run concurrently, but only " + memUsageThreads
- + " threads will be run due to limited memory; "
- + "increasing the amount of memory by using the -Xmx flag "
- + "at startup (java -Xmx" + suggestedMaxMemory
- + "M ...) may result in faster compiles");
- }
-
- result = Math.min(memUsageThreads, desiredThreads);
- return result;
+ PermutationCompiler permCompiler = new PermutationCompiler(logger, jjs,
+ perms);
+ permCompiler.go(linkerContext);
}
/**
diff --git a/dev/core/src/com/google/gwt/dev/Permutation.java b/dev/core/src/com/google/gwt/dev/Permutation.java
new file mode 100644
index 0000000..64ae9f3
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/Permutation.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2008 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.gwt.dev;
+
+import com.google.gwt.dev.cfg.StaticPropertyOracle;
+import com.google.gwt.dev.jdt.RebindOracle;
+
+/**
+ * Represents the state of a single permutation for compile.
+ *
+ * @see PermutationCompiler
+ */
+public final class Permutation {
+ private final int number;
+ private final StaticPropertyOracle propertyOracle;
+ private final RebindOracle rebindOracle;
+
+ public Permutation(int number, RebindOracle rebindOracle,
+ StaticPropertyOracle propertyOracle) {
+ this.number = number;
+ this.rebindOracle = rebindOracle;
+ this.propertyOracle = propertyOracle;
+ }
+
+ public int getNumber() {
+ return number;
+ }
+
+ public StaticPropertyOracle getPropertyOracle() {
+ return propertyOracle;
+ }
+
+ public RebindOracle getRebindOracle() {
+ return rebindOracle;
+ }
+}
diff --git a/dev/core/src/com/google/gwt/dev/PermutationCompiler.java b/dev/core/src/com/google/gwt/dev/PermutationCompiler.java
new file mode 100644
index 0000000..db7eac3
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/PermutationCompiler.java
@@ -0,0 +1,433 @@
+/*
+ * Copyright 2008 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.gwt.dev;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.linker.SelectionProperty;
+import com.google.gwt.core.ext.linker.impl.StandardCompilationResult;
+import com.google.gwt.core.ext.linker.impl.StandardLinkerContext;
+import com.google.gwt.dev.cfg.Property;
+import com.google.gwt.dev.cfg.StaticPropertyOracle;
+import com.google.gwt.dev.jjs.JavaToJavaScriptCompiler;
+import com.google.gwt.dev.jjs.ast.JProgram;
+import com.google.gwt.dev.util.PerfLogger;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Compiles a set of permutations, possibly in parallel in multiple threads.
+ */
+public class PermutationCompiler {
+
+ /**
+ * A Result for a permutation that failed to compile.
+ */
+ private static final class FailedResult extends Result {
+ private Throwable exception;
+
+ public FailedResult(Permutation perm, Throwable exception) {
+ super(perm);
+ this.exception = exception;
+ }
+
+ public Throwable getException() {
+ return exception;
+ }
+ }
+
+ /**
+ * Represents the task of compiling a single permutation.
+ */
+ private static final class PermutationTask implements Callable<String> {
+ private static void logProperties(TreeLogger logger,
+ StaticPropertyOracle propOracle) {
+ Property[] props = propOracle.getOrderedProps();
+ String[] values = propOracle.getOrderedPropValues();
+ if (logger.isLoggable(TreeLogger.DEBUG)) {
+ logger = logger.branch(TreeLogger.DEBUG, "Setting properties", null);
+ for (int i = 0; i < props.length; i++) {
+ String name = props[i].getName();
+ String value = values[i];
+ logger.log(TreeLogger.TRACE, name + " = " + value, null);
+ }
+ }
+ }
+
+ private final JavaToJavaScriptCompiler jjs;
+ private final TreeLogger logger;
+ private final Permutation perm;
+
+ public PermutationTask(TreeLogger logger, JavaToJavaScriptCompiler jjs,
+ Permutation perm) {
+ this.logger = logger;
+ this.jjs = jjs;
+ this.perm = perm;
+ }
+
+ public String call() throws Exception {
+ PerfLogger.start("Permutation #" + (perm.getNumber() + 1));
+ try {
+ TreeLogger branch = logger.branch(TreeLogger.TRACE,
+ "Analyzing permutation #" + (perm.getNumber() + 1));
+ logProperties(branch, perm.getPropertyOracle());
+ return jjs.compile(branch, perm.getRebindOracle());
+ } finally {
+ PerfLogger.end();
+ }
+ }
+
+ public Permutation getPermutation() {
+ return perm;
+ }
+ }
+
+ /**
+ * Contains the results of an attempt to compile.
+ */
+ private abstract static class Result {
+ private final Permutation perm;
+
+ public Result(Permutation perm) {
+ this.perm = perm;
+ }
+
+ public Permutation getPermutation() {
+ return perm;
+ }
+ }
+
+ /**
+ * A Result for a permutation that succeeded.
+ */
+ private static final class SuccessResult extends Result {
+ private final String js;
+
+ public SuccessResult(Permutation perm, String js) {
+ super(perm);
+ this.js = js;
+ }
+
+ public String getJs() {
+ return js;
+ }
+ }
+
+ /**
+ * Implements a memory-sensitive worker thread to compile permutations.
+ */
+ private class WorkerThread implements Runnable {
+ private PermutationTask currentTask;
+
+ private final Runnable outOfMemoryRetryAction = new Runnable() {
+ public void run() {
+ currentTask.logger.log(
+ TreeLogger.WARN,
+ "Not enough memory to run another concurrent permutation, reducing thread count; "
+ + "increasing the amount of memory by using the -Xmx flag "
+ + "at startup may result in faster compiles");
+ tasks.add(currentTask);
+ }
+ };
+
+ public void run() {
+ while (true) {
+ currentTask = tasks.poll();
+ if (currentTask == null) {
+ // Nothing left to do.
+ tryToExitNonFinalThread(null);
+
+ // As the last thread, I must inform the main thread we're all done.
+ results.add(FINISHED_RESULT);
+ exitFinalThread();
+ }
+
+ if (!hasEnoughMemory()) {
+ /*
+ * Not enough memory to run, but if there are multiple threads, we can
+ * try again with fewer threads.
+ */
+ tryToExitNonFinalThread(outOfMemoryRetryAction);
+ }
+
+ boolean definitelyFinalThread = (threadCount.get() == 1);
+ try {
+ String result = currentTask.call();
+ results.add(new SuccessResult(currentTask.getPermutation(), result));
+ } catch (OutOfMemoryError e) {
+ if (definitelyFinalThread) {
+ // OOM on the final thread, this is a truly unrecoverable failure.
+ currentTask.logger.log(TreeLogger.ERROR, "Out of memory", e);
+ results.add(new FailedResult(currentTask.getPermutation(),
+ new UnableToCompleteException()));
+ exitFinalThread();
+ }
+
+ /*
+ * Try the task again with fewer threads, it may not OOM this time.
+ */
+ tryToExitNonFinalThread(outOfMemoryRetryAction);
+
+ /*
+ * Okay, so we actually are the final thread. However, we weren't the
+ * final thread at the beginning of the compilation, so it's possible
+ * that a retry may now succeed with only one active thread. Let's
+ * optimistically retry one last time, and if this doesn't work, it's
+ * a hard failure.
+ */
+ outOfMemoryRetryAction.run();
+ } catch (Throwable e) {
+ // Unexpected error compiling, this is unrecoverable.
+ results.add(new FailedResult(currentTask.getPermutation(), e));
+ throw new ThreadDeath();
+ }
+ }
+ }
+
+ private void exitFinalThread() {
+ assert threadCount.compareAndSet(1, 0);
+ throw new ThreadDeath();
+ }
+
+ /**
+ * Returns <code>true</code> if there is enough estimated memory to run
+ * another permutation, or if this is the last live worker thread and we
+ * have no choice.
+ */
+ private boolean hasEnoughMemory() {
+ if (threadCount.get() == 1) {
+ // I'm the last thread, so I have to at least try.
+ return true;
+ }
+
+ if (astMemoryUsage >= getPotentialFreeMemory()) {
+ return true;
+ }
+
+ // Best effort memory reclaim.
+ System.gc();
+ return astMemoryUsage < getPotentialFreeMemory();
+ }
+
+ /**
+ * Exits this thread if and only if it's not the last running thread,
+ * performing the specified action before terminating.
+ *
+ * @param actionOnExit
+ */
+ private void tryToExitNonFinalThread(Runnable actionOnExit) {
+ int remainingThreads = threadCount.decrementAndGet();
+ if (remainingThreads == 0) {
+ // We are definitely the last thread.
+ threadCount.incrementAndGet();
+ return;
+ }
+
+ // We are definitely not the last thread, and have removed our count.
+ if (actionOnExit != null) {
+ actionOnExit.run();
+ }
+ throw new ThreadDeath();
+ }
+ }
+
+ /**
+ * A marker Result that tells the main thread all work is done.
+ */
+ private static final Result FINISHED_RESULT = new Result(null) {
+ };
+
+ private static long getPotentialFreeMemory() {
+ long used = Runtime.getRuntime().totalMemory()
+ - Runtime.getRuntime().freeMemory();
+ assert (used > 0);
+ long potentialFree = Runtime.getRuntime().maxMemory() - used;
+ assert (potentialFree >= 0);
+ return potentialFree;
+ }
+
+ /**
+ * Holds an estimate of how many bytes of memory a new concurrent compilation
+ * will consume.
+ */
+ protected final long astMemoryUsage;
+
+ /**
+ * A queue of results being sent from worker threads to the main thread.
+ */
+ protected final BlockingQueue<Result> results = new LinkedBlockingQueue<Result>();
+
+ /**
+ * A queue of tasks being sent to the worker threads.
+ */
+ protected final ConcurrentLinkedQueue<PermutationTask> tasks = new ConcurrentLinkedQueue<PermutationTask>();
+
+ /**
+ * Tracks the number of live worker threads.
+ */
+ protected final AtomicInteger threadCount = new AtomicInteger();
+
+ private final TreeLogger logger;
+
+ public PermutationCompiler(TreeLogger logger, JavaToJavaScriptCompiler jjs,
+ Permutation[] perms) {
+ this.logger = logger.branch(TreeLogger.DEBUG, "Compiling " + perms.length
+ + " permutations");
+ this.astMemoryUsage = jjs.getAstMemoryUsage();
+ for (Permutation perm : perms) {
+ tasks.add(new PermutationTask(this.logger, jjs, perm));
+ }
+ }
+
+ public void go(StandardLinkerContext linkerContext)
+ throws UnableToCompleteException {
+ int initialThreadCount = computeInitialThreadCount();
+ Thread[] workerThreads = new Thread[initialThreadCount];
+ for (int i = 0; i < initialThreadCount; ++i) {
+ workerThreads[i] = new Thread(new WorkerThread());
+ }
+ threadCount.set(initialThreadCount);
+ for (Thread thread : workerThreads) {
+ thread.start();
+ }
+ try {
+ while (true) {
+ Result result = results.take();
+ if (result == FINISHED_RESULT) {
+ assert threadCount.get() == 0;
+ return;
+ } else if (result instanceof SuccessResult) {
+ finishPermuation(logger, linkerContext, (SuccessResult) result);
+ } else if (result instanceof FailedResult) {
+ FailedResult failedResult = (FailedResult) result;
+ throw logAndTranslateException(failedResult.getException());
+ }
+ // Allow GC.
+ result = null;
+ }
+
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Unexpected interruption", e);
+ } finally {
+ for (Thread thread : workerThreads) {
+ if (thread.isAlive()) {
+ thread.interrupt();
+ }
+ }
+ }
+ }
+
+ private int computeInitialThreadCount() {
+ /*
+ * Don't need more threads than the number of permutations.
+ */
+ int result = tasks.size();
+
+ /*
+ * Computation is mostly CPU bound, so don't use more threads than
+ * processors.
+ */
+ result = Math.min(Runtime.getRuntime().availableProcessors(), result);
+
+ /*
+ * Allow user-defined override as an escape valve.
+ */
+ result = Math.min(result, Integer.getInteger("gwt.jjs.maxThreads", result));
+
+ if (result == 1) {
+ return 1;
+ }
+
+ // More than one thread would definitely be faster at this point.
+
+ if (JProgram.isTracingEnabled()) {
+ logger.log(TreeLogger.INFO,
+ "Parallel compilation disabled due to gwt.jjs.traceMethods being enabled");
+ return 1;
+ }
+
+ int desiredThreads = result;
+
+ /*
+ * Need to do some memory estimation to figure out how many concurrent
+ * threads we can safely run.
+ */
+ long potentialFreeMemory = getPotentialFreeMemory();
+ int extraMemUsageThreads = (int) (potentialFreeMemory / astMemoryUsage);
+ logger.log(TreeLogger.TRACE,
+ "Extra threads constrained by estimated memory usage: "
+ + extraMemUsageThreads + " = " + potentialFreeMemory + " / "
+ + astMemoryUsage);
+ int memUsageThreads = extraMemUsageThreads + 1;
+
+ if (memUsageThreads < desiredThreads) {
+ long currentMaxMemory = Runtime.getRuntime().maxMemory();
+ // Convert to megabytes.
+ currentMaxMemory /= 1024 * 1024;
+
+ long suggestedMaxMemory = currentMaxMemory * 2;
+
+ logger.log(TreeLogger.WARN, desiredThreads
+ + " threads could be run concurrently, but only " + memUsageThreads
+ + " threads will be run due to limited memory; "
+ + "increasing the amount of memory by using the -Xmx flag "
+ + "at startup (java -Xmx" + suggestedMaxMemory
+ + "M ...) may result in faster compiles");
+ }
+
+ result = Math.min(memUsageThreads, desiredThreads);
+ return result;
+ }
+
+ private void finishPermuation(TreeLogger logger,
+ StandardLinkerContext linkerContext, SuccessResult result)
+ throws UnableToCompleteException {
+ Permutation perm = result.getPermutation();
+ StandardCompilationResult compilation = linkerContext.getCompilation(
+ logger, result.getJs());
+ StaticPropertyOracle propOracle = perm.getPropertyOracle();
+ Property[] orderedProps = propOracle.getOrderedProps();
+ String[] orderedPropValues = propOracle.getOrderedPropValues();
+ Map<SelectionProperty, String> unboundProperties = new HashMap<SelectionProperty, String>();
+ for (int i = 0; i < orderedProps.length; i++) {
+ SelectionProperty key = linkerContext.getProperty(orderedProps[i].getName());
+ if (key.tryGetValue() != null) {
+ /*
+ * The view of the Permutation doesn't include properties with defined
+ * values.
+ */
+ continue;
+ }
+ unboundProperties.put(key, orderedPropValues[i]);
+ }
+ compilation.addSelectionPermutation(unboundProperties);
+ }
+
+ private UnableToCompleteException logAndTranslateException(Throwable e) {
+ if (e instanceof UnableToCompleteException) {
+ return (UnableToCompleteException) e;
+ } else {
+ logger.log(TreeLogger.ERROR, "Unexpected compiler failure", e);
+ return new UnableToCompleteException();
+ }
+ }
+}
diff --git a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
index baf9709..49b59fa 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
@@ -443,116 +443,40 @@
jsProgram = savedJsProgram;
savedJProgram = null;
savedJsProgram = null;
- }
- }
-
- if (jprogram == null || jsProgram == null) {
- if (serializedAst == null) {
- throw new IllegalStateException("No serialized AST was cached.");
- }
- try {
- PerfLogger.start("deserialize");
- ByteArrayInputStream bais = new ByteArrayInputStream(serializedAst);
- ObjectInputStream is;
- is = new ObjectInputStream(bais);
- jprogram = (JProgram) is.readObject();
- jsProgram = (JsProgram) is.readObject();
- PerfLogger.end();
- } catch (IOException e) {
- throw new RuntimeException(
- "Should be impossible for memory based streams", e);
- } catch (ClassNotFoundException e) {
- throw new RuntimeException(
- "Should be impossible when deserializing in process", e);
+ } else {
+ if (serializedAst == null) {
+ throw new IllegalStateException("No serialized AST was cached.");
+ }
+ try {
+ /*
+ * Force all AST deserializations to occur in sequence; this reduces
+ * the chance of multiple threads going OOM at the same time.
+ */
+ synchronized (myLockObject) {
+ PerfLogger.start("deserialize");
+ ByteArrayInputStream bais = new ByteArrayInputStream(serializedAst);
+ ObjectInputStream is;
+ is = new ObjectInputStream(bais);
+ jprogram = (JProgram) is.readObject();
+ jsProgram = (JsProgram) is.readObject();
+ PerfLogger.end();
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(
+ "Should be impossible for memory based streams", e);
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException(
+ "Should be impossible when deserializing in process", e);
+ }
}
}
try {
- if (JProgram.isTracingEnabled()) {
- System.out.println("------------------------------------------------------------");
- System.out.println("| (new permuation) |");
- System.out.println("------------------------------------------------------------");
- }
-
- ResolveRebinds.exec(logger, jprogram, rebindOracle);
-
- // (4) Optimize the normalized Java AST for each permutation.
- optimize(jprogram);
-
- // (5) "Normalize" the high-level Java tree into a lower-level tree more
- // suited for JavaScript code generation. Don't go reordering these
- // willy-nilly because there are some subtle interdependencies.
- LongCastNormalizer.exec(jprogram);
- JsoDevirtualizer.exec(jprogram);
- CatchBlockNormalizer.exec(jprogram);
- PostOptimizationCompoundAssignmentNormalizer.exec(jprogram);
- LongEmulationNormalizer.exec(jprogram);
- CastNormalizer.exec(jprogram);
- ArrayNormalizer.exec(jprogram);
- EqualityNormalizer.exec(jprogram);
-
- // (6) Perform further post-normalization optimizations
- // Prune everything
- Pruner.exec(jprogram, false);
-
- // (7) Generate a JavaScript code DOM from the Java type declarations
- jprogram.typeOracle.recomputeClinits();
- GenerateJavaScriptAST.exec(jprogram, jsProgram, options.getOutput());
-
- // Allow GC.
- jprogram = null;
-
- // (8) Normalize the JS AST.
- // Fix invalid constructs created during JS AST gen.
- JsNormalizer.exec(jsProgram);
- // Resolve all unresolved JsNameRefs.
- JsSymbolResolver.exec(jsProgram);
-
- // (9) Optimize the JS AST.
- if (options.isAggressivelyOptimize()) {
- boolean didChange;
- do {
- didChange = false;
- // Remove unused functions, possible
- didChange = JsStaticEval.exec(jsProgram) || didChange;
- // Inline JavaScript function invocations
- didChange = JsInliner.exec(jsProgram) || didChange;
- // Remove unused functions, possible
- didChange = JsUnusedFunctionRemover.exec(jsProgram) || didChange;
- } while (didChange);
- }
-
- // (10) Obfuscate
- switch (options.getOutput()) {
- case OBFUSCATED:
- JsStringInterner.exec(jsProgram);
- JsObfuscateNamer.exec(jsProgram);
- break;
- case PRETTY:
- // We don't intern strings in pretty mode to improve readability
- JsPrettyNamer.exec(jsProgram);
- break;
- case DETAILED:
- JsStringInterner.exec(jsProgram);
- JsVerboseNamer.exec(jsProgram);
- break;
- default:
- throw new InternalCompilerException("Unknown output mode");
- }
-
- // (11) Perform any post-obfuscation normalizations.
-
- // Work around an IE7 bug,
- // http://code.google.com/p/google-web-toolkit/issues/detail?id=1440
- JsIEBlockSizeVisitor.exec(jsProgram);
-
- // (12) Generate the final output text.
- DefaultTextOutput out = new DefaultTextOutput(
- options.getOutput().shouldMinimize());
- JsSourceGenerationVisitor v = new JsSourceGenerationVisitor(out);
- v.accept(jsProgram);
- return out.toString();
+ return doCompile(logger, jprogram, jsProgram, rebindOracle);
} catch (Throwable e) {
+ // Allow GC before logging exception in case it was an OOM.
+ jprogram = null;
+ jsProgram = null;
throw logAndTranslateException(logger, e);
}
}
@@ -561,9 +485,106 @@
return astMemoryUsage;
}
- protected void optimize(JProgram jprogram) {
+ protected String doCompile(TreeLogger logger, JProgram jprogram,
+ JsProgram jsProgram, RebindOracle rebindOracle)
+ throws InterruptedException {
+ if (JProgram.isTracingEnabled()) {
+ System.out.println("------------------------------------------------------------");
+ System.out.println("| (new permuation) |");
+ System.out.println("------------------------------------------------------------");
+ }
+
+ ResolveRebinds.exec(logger, jprogram, rebindOracle);
+
+ // (4) Optimize the normalized Java AST for each permutation.
+ optimize(jprogram);
+
+ // (5) "Normalize" the high-level Java tree into a lower-level tree more
+ // suited for JavaScript code generation. Don't go reordering these
+ // willy-nilly because there are some subtle interdependencies.
+ LongCastNormalizer.exec(jprogram);
+ JsoDevirtualizer.exec(jprogram);
+ CatchBlockNormalizer.exec(jprogram);
+ PostOptimizationCompoundAssignmentNormalizer.exec(jprogram);
+ LongEmulationNormalizer.exec(jprogram);
+ CastNormalizer.exec(jprogram);
+ ArrayNormalizer.exec(jprogram);
+ EqualityNormalizer.exec(jprogram);
+
+ // (6) Perform further post-normalization optimizations
+ // Prune everything
+ Pruner.exec(jprogram, false);
+
+ // (7) Generate a JavaScript code DOM from the Java type declarations
+ jprogram.typeOracle.recomputeClinits();
+ GenerateJavaScriptAST.exec(jprogram, jsProgram, options.getOutput());
+
+ // Allow GC.
+ jprogram = null;
+
+ // (8) Normalize the JS AST.
+ // Fix invalid constructs created during JS AST gen.
+ JsNormalizer.exec(jsProgram);
+ // Resolve all unresolved JsNameRefs.
+ JsSymbolResolver.exec(jsProgram);
+
+ // (9) Optimize the JS AST.
+ if (options.isAggressivelyOptimize()) {
+ boolean didChange;
+ do {
+ if (Thread.interrupted()) {
+ throw new InterruptedException();
+ }
+
+ didChange = false;
+ // Remove unused functions, possible
+ didChange = JsStaticEval.exec(jsProgram) || didChange;
+ // Inline JavaScript function invocations
+ didChange = JsInliner.exec(jsProgram) || didChange;
+ // Remove unused functions, possible
+ didChange = JsUnusedFunctionRemover.exec(jsProgram) || didChange;
+ } while (didChange);
+ }
+
+ // (10) Obfuscate
+ switch (options.getOutput()) {
+ case OBFUSCATED:
+ JsStringInterner.exec(jsProgram);
+ JsObfuscateNamer.exec(jsProgram);
+ break;
+ case PRETTY:
+ // We don't intern strings in pretty mode to improve readability
+ JsPrettyNamer.exec(jsProgram);
+ break;
+ case DETAILED:
+ JsStringInterner.exec(jsProgram);
+ JsVerboseNamer.exec(jsProgram);
+ break;
+ default:
+ throw new InternalCompilerException("Unknown output mode");
+ }
+
+ // (11) Perform any post-obfuscation normalizations.
+
+ // Work around an IE7 bug,
+ // http://code.google.com/p/google-web-toolkit/issues/detail?id=1440
+ JsIEBlockSizeVisitor.exec(jsProgram);
+
+ // (12) Generate the final output text.
+ DefaultTextOutput out = new DefaultTextOutput(
+ options.getOutput().shouldMinimize());
+ JsSourceGenerationVisitor v = new JsSourceGenerationVisitor(out);
+ v.accept(jsProgram);
+ return out.toString();
+ }
+
+ protected void optimize(JProgram jprogram) throws InterruptedException {
boolean didChange;
do {
+ if (Thread.interrupted()) {
+ throw new InterruptedException();
+ }
+
// Recompute clinits each time, they can become empty.
jprogram.typeOracle.recomputeClinits();
@@ -681,6 +702,9 @@
}
}
return new UnableToCompleteException();
+ } else if (e instanceof OutOfMemoryError) {
+ // Rethrow the original exception so the caller can deal with it.
+ throw (OutOfMemoryError) e;
} else {
logger.log(TreeLogger.ERROR, "Unexpected internal compiler error", e);
return new UnableToCompleteException();
diff --git a/dev/core/src/com/google/gwt/dev/js/ast/JsVisitor.java b/dev/core/src/com/google/gwt/dev/js/ast/JsVisitor.java
index a5a78fe..7a3cbc3 100644
--- a/dev/core/src/com/google/gwt/dev/js/ast/JsVisitor.java
+++ b/dev/core/src/com/google/gwt/dev/js/ast/JsVisitor.java
@@ -427,6 +427,9 @@
JsContext<T> ctx) {
try {
node.traverse(this, ctx);
+ } catch (OutOfMemoryError e) {
+ // Just rethrow, it's not our problem.
+ throw e;
} catch (Throwable e) {
throw translateException(node, e);
}