Merging releases/1.6@4130:4147 into trunk.

svn merge --accept postpone -r4130:4147 https://google-web-toolkit.googlecode.com/svn/releases/1.6 .



git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@4148 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardCompilationResult.java b/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardCompilationResult.java
index 2b4f0f8..716995e 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardCompilationResult.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardCompilationResult.java
@@ -17,6 +17,7 @@
 
 import com.google.gwt.core.ext.linker.CompilationResult;
 import com.google.gwt.core.ext.linker.SelectionProperty;
+import com.google.gwt.dev.PermutationResult;
 import com.google.gwt.dev.util.Util;
 
 import java.io.File;
@@ -68,19 +69,18 @@
    */
   public static final Comparator<SortedMap<SelectionProperty, String>> MAP_COMPARATOR = new MapComparator();
 
-  private final File cacheFile;
+  private final File resultFile;
   private transient SoftReference<String[]> js;
 
   private final SortedSet<SortedMap<SelectionProperty, String>> propertyValues = new TreeSet<SortedMap<SelectionProperty, String>>(
       MAP_COMPARATOR);
   private final String strongName;
 
-  public StandardCompilationResult(String[] js, String strongName,
-      File cacheFile) {
+  public StandardCompilationResult(String[] js, String strongName, File resultFile) {
     super(StandardLinkerContext.class);
     this.js = new SoftReference<String[]>(js);
     this.strongName = strongName;
-    this.cacheFile = cacheFile;
+    this.resultFile = resultFile;
   }
 
   /**
@@ -102,12 +102,20 @@
     }
 
     if (toReturn == null) {
-      byte[][] bytes = Util.readFileAndSplit(cacheFile);
-      if (bytes == null) {
-        throw new RuntimeException("Unexpectedly unable to read JS file '"
-            + cacheFile.getAbsolutePath() + "'");
+      PermutationResult result;
+      try {
+        result = Util.readFileAsObject(resultFile, PermutationResult.class);
+      } catch (ClassNotFoundException e) {
+        throw new RuntimeException(
+            "Unexpectedly unable to read PermutationResult '"
+                + resultFile.getAbsolutePath() + "'", e);
       }
-      toReturn = Util.toStrings(bytes);
+      if (result == null) {
+        throw new RuntimeException(
+            "Unexpectedly unable to read PermutationResult '"
+                + resultFile.getAbsolutePath() + "'");
+      }
+      toReturn = result.getJs();
       js = new SoftReference<String[]>(toReturn);
     }
     return toReturn;
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardLinkerContext.java b/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardLinkerContext.java
index 26353b1..cb9619a 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardLinkerContext.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardLinkerContext.java
@@ -26,6 +26,7 @@
 import com.google.gwt.core.ext.linker.PublicResource;
 import com.google.gwt.core.ext.linker.SelectionProperty;
 import com.google.gwt.core.ext.linker.LinkerOrder.Order;
+import com.google.gwt.dev.PermutationResult;
 import com.google.gwt.dev.cfg.BindingProperty;
 import com.google.gwt.dev.cfg.ModuleDef;
 import com.google.gwt.dev.cfg.Property;
@@ -261,21 +262,27 @@
   /**
    * Gets or creates a CompilationResult for the given JavaScript program.
    */
-  public StandardCompilationResult getCompilation(TreeLogger logger, File jsFile)
-      throws UnableToCompleteException {
-    byte[][] results = Util.readFileAndSplit(jsFile);
-
-    if (results == null) {
-      logger.log(TreeLogger.ERROR, "Unable to read file '"
-          + jsFile.getAbsolutePath() + "'");
+  public StandardCompilationResult getCompilation(TreeLogger logger,
+      File resultFile) throws UnableToCompleteException {
+    PermutationResult permutationResult;
+    try {
+      permutationResult = Util.readFileAsObject(resultFile,
+          PermutationResult.class);
+    } catch (ClassNotFoundException e) {
+      logger.log(TreeLogger.ERROR, "Unable to instantiate PermutationResult", e);
       throw new UnableToCompleteException();
     }
 
-    String strongName = Util.computeStrongName(results);
+    if (permutationResult == null) {
+      logger.log(TreeLogger.ERROR, "Unable to read PermutationResult");
+      throw new UnableToCompleteException();
+    }
+
+    String strongName = Util.computeStrongName(Util.getBytes(permutationResult.getJs()));
     StandardCompilationResult result = resultsByStrongName.get(strongName);
     if (result == null) {
-      result = new StandardCompilationResult(Util.toStrings(results), strongName,
-          jsFile);
+      result = new StandardCompilationResult(permutationResult.getJs(),
+          strongName, resultFile);
       resultsByStrongName.put(result.getStrongName(), result);
       artifacts.add(result);
     }
diff --git a/dev/core/src/com/google/gwt/dev/CompilePerms.java b/dev/core/src/com/google/gwt/dev/CompilePerms.java
index 55c5999..ec0e924 100644
--- a/dev/core/src/com/google/gwt/dev/CompilePerms.java
+++ b/dev/core/src/com/google/gwt/dev/CompilePerms.java
@@ -18,10 +18,10 @@
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.dev.CompileTaskRunner.CompileTask;
-import com.google.gwt.dev.PermutationCompiler.ResultsHandler;
-import com.google.gwt.dev.jjs.UnifiedAst;
 import com.google.gwt.dev.jjs.JavaToJavaScriptCompiler;
+import com.google.gwt.dev.jjs.UnifiedAst;
 import com.google.gwt.dev.util.Util;
+import com.google.gwt.dev.util.arg.ArgHandlerLocalWorkers;
 import com.google.gwt.util.tools.ArgHandlerString;
 
 import java.io.File;
@@ -29,15 +29,16 @@
 import java.util.TreeSet;
 
 /**
- * Performs the second phase of compilation, converting the Precompile's AST into
- * JavaScript outputs.
+ * Performs the second phase of compilation, converting the Precompile's AST
+ * into JavaScript outputs.
  */
 public class CompilePerms {
 
   /**
    * Options for CompilePerms.
    */
-  public interface CompilePermsOptions extends CompileTaskOptions, OptionPerms {
+  public interface CompilePermsOptions extends CompileTaskOptions,
+      OptionLocalWorkers, OptionPerms {
   }
 
   /**
@@ -121,6 +122,7 @@
     public ArgProcessor(CompilePermsOptions options) {
       super(options);
       registerHandler(new ArgHandlerPerms(options));
+      registerHandler(new ArgHandlerLocalWorkers(options));
     }
 
     @Override
@@ -135,6 +137,7 @@
   static class CompilePermsOptionsImpl extends CompileTaskOptionsImpl implements
       CompilePermsOptions {
 
+    private int localWorkers;
     private int[] permsToCompile = new int[0];
 
     public CompilePermsOptionsImpl() {
@@ -146,24 +149,40 @@
 
     public void copyFrom(CompilePermsOptions other) {
       super.copyFrom(other);
-
       setPermsToCompile(other.getPermsToCompile());
+      setLocalWorkers(other.getLocalWorkers());
+    }
+
+    public int getLocalWorkers() {
+      return localWorkers;
     }
 
     public int[] getPermsToCompile() {
       return permsToCompile.clone();
     }
 
+    public void setLocalWorkers(int localWorkers) {
+      this.localWorkers = localWorkers;
+    }
+
     public void setPermsToCompile(int[] permsToCompile) {
       this.permsToCompile = permsToCompile.clone();
     }
   }
 
-  public static String[] compile(TreeLogger logger, Permutation permutation,
-      UnifiedAst unifiedAst) {
+  /**
+   * Compile a single permutation.
+   */
+  public static PermutationResult compile(TreeLogger logger,
+      Permutation permutation, UnifiedAst unifiedAst) {
     try {
-      return JavaToJavaScriptCompiler.compilePermutation(logger, unifiedAst,
-          permutation.getRebindAnswers());
+      final String js[] = JavaToJavaScriptCompiler.compilePermutation(logger,
+          unifiedAst, permutation.getRebindAnswers());
+      return new PermutationResult() {
+        public String[] getJs() {
+          return js;
+        }
+      };
     } catch (UnableToCompleteException e) {
       // We intentionally don't pass in the exception here since the real
       // cause has been logged.
@@ -171,6 +190,20 @@
     }
   }
 
+  /**
+   * Compile multiple permutations.
+   */
+  public static boolean compile(TreeLogger logger,
+      Precompilation precompilation, Permutation[] perms, int localWorkers,
+      File[] resultFiles) throws UnableToCompleteException {
+    final TreeLogger branch = logger.branch(TreeLogger.INFO, "Compiling "
+        + perms.length + " permutations");
+    PermutationWorkerFactory.compilePermutations(logger, precompilation, perms,
+        localWorkers, resultFiles);
+    branch.log(TreeLogger.INFO, "Permutation compile succeeded");
+    return true;
+  }
+
   public static void main(String[] args) {
     /*
      * NOTE: main always exits with a call to System.exit to terminate any
@@ -194,6 +227,14 @@
     System.exit(1);
   }
 
+  public static File[] makeResultFiles(File compilerWorkDir, Permutation[] perms) {
+    File[] resultFiles = new File[perms.length];
+    for (int i = 0; i < perms.length; ++i) {
+      resultFiles[i] = makePermFilename(compilerWorkDir, perms[i].getId());
+    }
+    return resultFiles;
+  }
+
   /**
    * Return the filename corresponding to the given permutation number,
    * one-based.
@@ -229,17 +270,15 @@
           + precompilationFile.getAbsolutePath() + "'", e);
       return false;
     }
-    Permutation[] perms = precompilation.getPermutations();
-    UnifiedAst unifiedAst = precompilation.getUnifiedAst();
-    int[] permsToRun = options.getPermsToCompile();
 
+    Permutation[] perms = precompilation.getPermutations();
+    Permutation[] subPerms;
+    int[] permsToRun = options.getPermsToCompile();
     if (permsToRun.length == 0) {
-      // Compile them all.
-      permsToRun = new int[perms.length];
-      for (int i = 0; i < permsToRun.length; ++i) {
-        permsToRun[i] = i;
-      }
+      subPerms = perms;
     } else {
+      int i = 0;
+      subPerms = new Permutation[permsToRun.length];
       // Range check the supplied perms.
       for (int permToRun : permsToRun) {
         if (permToRun >= perms.length) {
@@ -248,21 +287,12 @@
               + (perms.length - 1) + "'");
           return false;
         }
+        subPerms[i++] = perms[permToRun];
       }
     }
 
-    final TreeLogger branch = logger.branch(TreeLogger.INFO, "Compiling "
-        + permsToRun.length + " permutations");
-    PermutationCompiler multiThread = new PermutationCompiler(branch,
-        unifiedAst, perms, permsToRun);
-    multiThread.go(new ResultsHandler() {
-      public void addResult(Permutation permutation, int permNum, String[] js)
-          throws UnableToCompleteException {
-        Util.writeStringsAsFile(branch, makePermFilename(
-            options.getCompilerWorkDir(), permNum), js);
-      }
-    });
-    branch.log(TreeLogger.INFO, "Permutation compile succeeded");
-    return true;
+    File[] resultFiles = makeResultFiles(options.getCompilerWorkDir(), perms);
+    return compile(logger, precompilation, subPerms, options.getLocalWorkers(),
+        resultFiles);
   }
 }
diff --git a/dev/core/src/com/google/gwt/dev/CompilePermsServer.java b/dev/core/src/com/google/gwt/dev/CompilePermsServer.java
new file mode 100644
index 0000000..ace3385
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/CompilePermsServer.java
@@ -0,0 +1,309 @@
+/*
+ * 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.TreeLogger.Type;
+import com.google.gwt.dev.jjs.UnifiedAst;
+import com.google.gwt.dev.util.arg.ArgHandlerLogLevel;
+import com.google.gwt.dev.util.arg.OptionLogLevel;
+import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
+import com.google.gwt.util.tools.ArgHandlerString;
+import com.google.gwt.util.tools.ToolBase;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.net.Socket;
+import java.net.UnknownHostException;
+
+/**
+ * An out-of-process implementation of CompilePerms that will connect back to an
+ * existing compiler host. This class is intended to be launched by
+ * {@link ExternalPermutationWorkerFactory} and not by users directly.
+ */
+public class CompilePermsServer {
+  /**
+   * Adds host and port information.
+   */
+  public interface CompileServerOptions extends OptionLogLevel {
+    String getCompileHost();
+
+    int getCompilePort();
+
+    String getCookie();
+
+    void setCompileHost(String host);
+
+    void setCompilePort(int port);
+
+    void setCookie(String cookie);
+  }
+
+  static final class ArgHandlerCompileHost extends ArgHandlerString {
+    private final CompileServerOptions options;
+
+    public ArgHandlerCompileHost(CompileServerOptions options) {
+      this.options = options;
+    }
+
+    @Override
+    public String getPurpose() {
+      return "The host to which the permutation server should connect";
+    }
+
+    @Override
+    public String getTag() {
+      return "-host";
+    }
+
+    @Override
+    public String[] getTagArgs() {
+      return new String[] {"hostname"};
+    }
+
+    @Override
+    public boolean isRequired() {
+      return true;
+    }
+
+    @Override
+    public boolean setString(String str) {
+      options.setCompileHost(str);
+      return true;
+    }
+  }
+
+  static final class ArgHandlerCompilePort extends ArgHandlerString {
+    private final CompileServerOptions options;
+
+    public ArgHandlerCompilePort(CompileServerOptions options) {
+      this.options = options;
+    }
+
+    @Override
+    public String getPurpose() {
+      return "The port to which the permutation server should connect";
+    }
+
+    @Override
+    public String getTag() {
+      return "-port";
+    }
+
+    @Override
+    public String[] getTagArgs() {
+      return new String[] {"1234"};
+    }
+
+    @Override
+    public boolean isRequired() {
+      return true;
+    }
+
+    @Override
+    public boolean setString(String str) {
+      Integer port = Integer.parseInt(str);
+      if (port <= 0) {
+        return false;
+      }
+      options.setCompilePort(port);
+      return true;
+    }
+  }
+
+  static final class ArgHandlerCookie extends ArgHandlerString {
+
+    private final CompileServerOptions options;
+
+    public ArgHandlerCookie(CompileServerOptions option) {
+      this.options = option;
+    }
+
+    @Override
+    public String getPurpose() {
+      return "Specifies the security cookie the server expects";
+    }
+
+    @Override
+    public String getTag() {
+      return "-cookie";
+    }
+
+    @Override
+    public String[] getTagArgs() {
+      return new String[] {"cookie"};
+    }
+
+    @Override
+    public boolean isRequired() {
+      return true;
+    }
+
+    @Override
+    public boolean setString(String str) {
+      options.setCookie(str);
+      return true;
+    }
+  }
+
+  static class ArgProcessor extends ToolBase {
+    public ArgProcessor(CompileServerOptions options) {
+      registerHandler(new ArgHandlerLogLevel(options));
+      registerHandler(new ArgHandlerCompileHost(options));
+      registerHandler(new ArgHandlerCompilePort(options));
+      registerHandler(new ArgHandlerCookie(options));
+    }
+
+    /*
+     * Overridden to make public.
+     */
+    @Override
+    public final boolean processArgs(String[] args) {
+      return super.processArgs(args);
+    }
+
+    @Override
+    protected String getName() {
+      return CompilePermsServer.class.getName();
+    }
+  }
+
+  static class CompileServerOptionsImpl implements CompileServerOptions {
+
+    private String compileHost;
+    private int compilePort;
+    private String cookie;
+    private Type logLevel;
+
+    public void copyFrom(CompileServerOptions other) {
+      setCompileHost(other.getCompileHost());
+      setCompilePort(other.getCompilePort());
+      setCookie(other.getCookie());
+      setLogLevel(other.getLogLevel());
+    }
+
+    public String getCompileHost() {
+      return compileHost;
+    }
+
+    public int getCompilePort() {
+      return compilePort;
+    }
+
+    public String getCookie() {
+      return cookie;
+    }
+
+    public Type getLogLevel() {
+      return logLevel;
+    }
+
+    public void setCompileHost(String host) {
+      assert host != null;
+      compileHost = host;
+    }
+
+    public void setCompilePort(int port) {
+      assert port > 0;
+      compilePort = port;
+    }
+
+    public void setCookie(String cookie) {
+      this.cookie = cookie;
+    }
+
+    public void setLogLevel(Type logLevel) {
+      this.logLevel = logLevel;
+    }
+  }
+
+  public static void main(String[] args) {
+    final CompileServerOptions options = new CompileServerOptionsImpl();
+    if (new ArgProcessor(options).processArgs(args)) {
+      PrintWriterTreeLogger logger = new PrintWriterTreeLogger();
+      logger.setMaxDetail(options.getLogLevel());
+      if (run(options, logger)) {
+        System.exit(0);
+      }
+    }
+    System.exit(-1);
+  }
+
+  public static boolean run(CompileServerOptions options, TreeLogger logger) {
+    try {
+      Socket s = new Socket(options.getCompileHost(), options.getCompilePort());
+      logger.log(TreeLogger.DEBUG, "Socket opened");
+
+      ObjectOutputStream out = new ObjectOutputStream(s.getOutputStream());
+      ObjectInputStream in = new ObjectInputStream(s.getInputStream());
+
+      // Write my cookie
+      out.writeUTF(options.getCookie());
+      out.flush();
+
+      // Read the File that contains the serialized UnifiedAst
+      File astFile = (File) in.readObject();
+      ObjectInputStream astIn = new ObjectInputStream(new FileInputStream(
+          astFile));
+      UnifiedAst ast = (UnifiedAst) astIn.readObject();
+      ast.prepare();
+      logger.log(TreeLogger.SPAM, "Created new UnifiedAst instance");
+
+      // Report on the amount of memory we think we're using
+      long estimatedMemory = Runtime.getRuntime().totalMemory()
+          - Runtime.getRuntime().freeMemory();
+      out.writeLong(estimatedMemory);
+      out.flush();
+
+      boolean keepGoing = in.readBoolean();
+      while (keepGoing) {
+        compilePermutation(logger, ast, in, out);
+
+        keepGoing = in.readBoolean();
+        logger.log(TreeLogger.SPAM, "keepGoing = " + keepGoing);
+      }
+
+      logger.log(TreeLogger.DEBUG, "Successfully terminating");
+      return true;
+
+    } catch (UnknownHostException e) {
+      logger.log(TreeLogger.ERROR, "Invalid hostname", e);
+    } catch (IOException e) {
+      logger.log(TreeLogger.ERROR, "Communication error", e);
+    } catch (ClassNotFoundException e) {
+      logger.log(TreeLogger.ERROR, "Probable client/server mismatch or "
+          + "classpath misconfiguration", e);
+    }
+    return false;
+  }
+
+  static void compilePermutation(TreeLogger logger, UnifiedAst ast,
+      ObjectInputStream in, ObjectOutputStream out)
+      throws ClassNotFoundException, IOException {
+    Permutation p = (Permutation) in.readObject();
+    logger.log(TreeLogger.SPAM, "Permutation read");
+
+    PermutationResult result = CompilePerms.compile(logger.branch(
+        TreeLogger.DEBUG, "Compiling"), p, ast);
+    logger.log(TreeLogger.DEBUG, "Successfully compiled permutation");
+    out.writeObject(result);
+    out.flush();
+    logger.log(TreeLogger.SPAM, "Sent result");
+  }
+}
diff --git a/dev/core/src/com/google/gwt/dev/CompileTaskOptionsImpl.java b/dev/core/src/com/google/gwt/dev/CompileTaskOptionsImpl.java
index e0b7ead..e92d1c8 100644
--- a/dev/core/src/com/google/gwt/dev/CompileTaskOptionsImpl.java
+++ b/dev/core/src/com/google/gwt/dev/CompileTaskOptionsImpl.java
@@ -24,8 +24,6 @@
  */
 class CompileTaskOptionsImpl implements CompileTaskOptions {
 
-  public static final String GWT_TMP_DIR = "gwt-tmp";
-
   private Type logLevel;
   private String moduleName;
   private boolean useGuiLogger;
diff --git a/dev/core/src/com/google/gwt/dev/CompilerOptions.java b/dev/core/src/com/google/gwt/dev/CompilerOptions.java
index 0b568e4..382eee8 100644
--- a/dev/core/src/com/google/gwt/dev/CompilerOptions.java
+++ b/dev/core/src/com/google/gwt/dev/CompilerOptions.java
@@ -21,5 +21,6 @@
 /**
  * The complete set of options for the GWT compiler.
  */
-public interface CompilerOptions extends PrecompileOptions, LinkOptions {
+public interface CompilerOptions extends PrecompileOptions, LinkOptions,
+    OptionLocalWorkers {
 }
diff --git a/dev/core/src/com/google/gwt/dev/ExternalPermutationWorkerFactory.java b/dev/core/src/com/google/gwt/dev/ExternalPermutationWorkerFactory.java
new file mode 100644
index 0000000..392afea
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/ExternalPermutationWorkerFactory.java
@@ -0,0 +1,376 @@
+/*
+ * 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.dev.jjs.UnifiedAst;
+import com.google.gwt.dev.util.Util;
+
+import java.io.BufferedReader;
+import java.io.EOFException;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.lang.management.ManagementFactory;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+
+/**
+ * A PermutationWorkerFactory designed to launch instances of
+ * {@link CompilePermsServer}. The system property
+ * {@value #JAVA_COMMAND_PROPERTY} can be used to change the command used to
+ * launch the JVM. The system property {@link #JVM_ARGS_PROPERTY} can be used to
+ * override the JVM args passed to the subprocess.
+ */
+public class ExternalPermutationWorkerFactory extends PermutationWorkerFactory {
+
+  /**
+   * Allows accept() to be called a finite number of times on a ServerSocket
+   * before closing the socket.
+   */
+  private static class CountedServerSocket {
+    private int accepts;
+    private ServerSocket sock;
+
+    public CountedServerSocket(ServerSocket sock, int maxAccepts) {
+      assert sock != null;
+      assert maxAccepts >= 1;
+
+      this.accepts = maxAccepts;
+      this.sock = sock;
+    }
+
+    public synchronized Socket accept() throws IOException {
+      assert accepts >= 0;
+
+      if (accepts == 0) {
+        throw new IllegalStateException("Too many calls to accept()");
+      }
+
+      try {
+        return sock.accept();
+      } finally {
+        if (--accepts == 0) {
+          sock.close();
+          sock = null;
+        }
+      }
+    }
+  }
+
+  private static class ExternalPermutationWorker implements PermutationWorker {
+    private final File astFile;
+    private final Set<String> cookies;
+    private ObjectInputStream in;
+    private ObjectOutputStream out;
+    private final CountedServerSocket serverSocket;
+    private Socket workerSocket;
+
+    public ExternalPermutationWorker(CountedServerSocket sock, File astFile,
+        Set<String> cookies) {
+      this.astFile = astFile;
+      this.cookies = cookies;
+      this.serverSocket = sock;
+    }
+
+    public PermutationResult compile(TreeLogger logger, Permutation permutation)
+        throws TransientWorkerException, UnableToCompleteException {
+
+      // If we've just started, we need to get a connection from a subprocess
+      if (workerSocket == null) {
+        try {
+          /*
+           * We've set SO_TIMEOUT, so this may fail if the remote process never
+           * connects back.
+           */
+          workerSocket = serverSocket.accept();
+
+          in = new ObjectInputStream(workerSocket.getInputStream());
+          out = new ObjectOutputStream(workerSocket.getOutputStream());
+
+          // Verify we're talking to the right worker
+          String c = in.readUTF();
+          if (!cookies.contains(c)) {
+            throw new TransientWorkerException("Received unknown cookie " + c,
+                null);
+          }
+
+          out.writeObject(astFile);
+
+          // Get the remote worker's estimate of memory use
+          long memoryUse = in.readLong();
+          logger.log(TreeLogger.SPAM, "Remote process indicates " + memoryUse
+              + " bytes of memory used");
+
+        } catch (SocketTimeoutException e) {
+          throw new TransientWorkerException(
+              "Remote process did not connect within timeout period", e);
+        } catch (IOException e) {
+          throw new TransientWorkerException(
+              "Unable to communicate with worker", e);
+        }
+      }
+
+      try {
+        out.writeBoolean(true);
+        out.writeObject(permutation);
+        out.flush();
+        return (PermutationResult) in.readObject();
+      } catch (IOException e) {
+        logger.log(TreeLogger.WARN, "Lost communication with remote process", e);
+        throw new TransientWorkerException(
+            "Lost communication with remote process", e);
+      } catch (ClassNotFoundException e) {
+        logger.log(TreeLogger.ERROR, "Unable to receive response", e);
+        throw new UnableToCompleteException();
+      }
+    }
+
+    public String getName() {
+      return "External worker "
+          + (workerSocket != null ? workerSocket.getRemoteSocketAddress()
+              : "unconnected");
+    }
+
+    public void shutdown() {
+      if (out != null) {
+        try {
+          out.writeBoolean(false);
+          out.flush();
+          out.close();
+        } catch (IOException e) {
+          // Not much to do here
+        }
+      }
+
+      if (in != null) {
+        try {
+          in.close();
+        } catch (IOException e) {
+          // Not much to do here
+        }
+      }
+
+      if (workerSocket != null) {
+        try {
+          workerSocket.close();
+        } catch (IOException e) {
+          // Nothing to do
+        }
+      }
+    }
+  }
+
+  /**
+   * A system property that can be used to override the command used to invoke a
+   * JVM instance.
+   */
+  public static final String JAVA_COMMAND_PROPERTY = "gwt.jjs.javaCommand";
+
+  /**
+   * A system property that can be used to override the JVM args passed to the
+   * subprocess.
+   */
+  public static final String JVM_ARGS_PROPERTY = "gwt.jjs.javaArgs";
+
+  /**
+   * Launches an external worker and returns the cookie that worker should
+   * return via the network connection.
+   */
+  private static String launchExternalWorker(TreeLogger logger, int port)
+      throws UnableToCompleteException {
+
+    String javaCommand = System.getProperty(JAVA_COMMAND_PROPERTY,
+        System.getProperty("java.home") + File.separator + "bin"
+            + File.separator + "java");
+    logger.log(TreeLogger.TRACE, "javaCommand = " + javaCommand);
+
+    // Construct the arguments
+    List<String> args = new ArrayList<String>();
+    args.add(javaCommand);
+
+    // This will include -Xmx, -D, etc...
+    String userJvmArgs = System.getProperty(JVM_ARGS_PROPERTY);
+    if (userJvmArgs == null) {
+      args.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments());
+    } else {
+      args.addAll(Arrays.asList(userJvmArgs.split(" ")));
+    }
+
+    // Determine the logLevel for the external program
+    TreeLogger.Type logLevel = TreeLogger.ERROR;
+    for (TreeLogger.Type t : TreeLogger.Type.values()) {
+      if (logger.isLoggable(t)) {
+        logLevel = t;
+      } else {
+        break;
+      }
+    }
+
+    byte[] cookieBytes = new byte[16];
+    (new Random()).nextBytes(cookieBytes);
+    String cookie = Util.toHexString(cookieBytes);
+
+    // Cook up the classpath, main class, and extra args
+    args.addAll(Arrays.asList("-classpath",
+        ManagementFactory.getRuntimeMXBean().getClassPath(),
+        CompilePermsServer.class.getName(), "-host", "localhost", "-port",
+        String.valueOf(port), "-logLevel", logLevel.toString(), "-cookie",
+        cookie));
+
+    // Filter undesirable arguments
+    for (Iterator<String> iter = args.iterator(); iter.hasNext();) {
+      String arg = iter.next();
+      if (arg.startsWith("-agentlib")) {
+        iter.remove();
+      }
+    }
+
+    ProcessBuilder builder = new ProcessBuilder();
+    builder.command(args);
+
+    try {
+      final Process proc = builder.start();
+      final BufferedReader bin = new BufferedReader(new InputStreamReader(
+          proc.getInputStream()));
+      final BufferedReader berr = new BufferedReader(new InputStreamReader(
+          proc.getErrorStream()));
+      final TreeLogger procLogger = logger.branch(TreeLogger.DEBUG,
+          "Process output");
+
+      // Threads to copy stdout, stderr to the logger
+      new Thread(new Runnable() {
+        public void run() {
+          while (true) {
+            try {
+              String line = bin.readLine();
+              if (line == null) {
+                break;
+              }
+              procLogger.log(TreeLogger.INFO, line);
+            } catch (EOFException e) {
+              // Ignore
+            } catch (IOException e) {
+              procLogger.log(TreeLogger.ERROR,
+                  "Unable to read from subprocess", e);
+            }
+          }
+        }
+      }).start();
+
+      new Thread(new Runnable() {
+        public void run() {
+          while (true) {
+            try {
+              String line = berr.readLine();
+              if (line == null) {
+                break;
+              }
+              procLogger.log(TreeLogger.ERROR, line);
+            } catch (EOFException e) {
+              // Ignore
+            } catch (IOException e) {
+              procLogger.log(TreeLogger.ERROR,
+                  "Unable to read from subprocess", e);
+            }
+          }
+        }
+      }).start();
+
+      // The child process should not outlive this JVM
+      Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
+        public void run() {
+          try {
+            proc.exitValue();
+          } catch (IllegalThreadStateException e) {
+            proc.destroy();
+          }
+        }
+      }));
+
+      return cookie;
+    } catch (IOException e) {
+      logger.log(TreeLogger.ERROR, "Unable to start external process", e);
+      throw new UnableToCompleteException();
+    }
+  }
+
+  private ServerSocket sock;
+
+  @Override
+  public Collection<PermutationWorker> getWorkers(TreeLogger logger,
+      UnifiedAst unifiedAst, int numWorkers) throws UnableToCompleteException {
+    File astFile;
+    try {
+      astFile = File.createTempFile("externalPermutationWorkerFactory", ".ser");
+      astFile.deleteOnExit();
+      Util.writeObjectAsFile(logger, astFile, unifiedAst);
+    } catch (IOException e) {
+      logger.log(TreeLogger.ERROR, "Unable to create temporary file", e);
+      throw new UnableToCompleteException();
+    }
+
+    Set<String> cookies = Collections.synchronizedSet(new HashSet<String>(
+        numWorkers));
+    CountedServerSocket countedSock = new CountedServerSocket(sock, numWorkers);
+    List<PermutationWorker> toReturn = new ArrayList<PermutationWorker>(
+        numWorkers);
+
+    for (int i = 0; i < numWorkers; i++) {
+      String cookie = launchExternalWorker(logger, sock.getLocalPort());
+      cookies.add(cookie);
+      toReturn.add(new ExternalPermutationWorker(countedSock, astFile, cookies));
+    }
+
+    return toReturn;
+  }
+
+  @Override
+  public void init(TreeLogger logger) throws UnableToCompleteException {
+    try {
+      sock = new ServerSocket();
+      /*
+       * Have accept() wait no more than one minute for a connection. This
+       * prevents dead-head behavior.
+       */
+      sock.setSoTimeout(60000);
+      sock.bind(null);
+      logger.log(TreeLogger.SPAM, "Listening for external workers on port "
+          + sock.getLocalPort());
+    } catch (IOException e) {
+      logger.log(TreeLogger.ERROR, "Unable to create socket", e);
+      throw new UnableToCompleteException();
+    }
+  }
+
+  @Override
+  public boolean isLocal() {
+    return true;
+  }
+}
diff --git a/dev/core/src/com/google/gwt/dev/GWTCompiler.java b/dev/core/src/com/google/gwt/dev/GWTCompiler.java
index 3d7b06d..546e5ab 100644
--- a/dev/core/src/com/google/gwt/dev/GWTCompiler.java
+++ b/dev/core/src/com/google/gwt/dev/GWTCompiler.java
@@ -17,13 +17,15 @@
 
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
-import com.google.gwt.dev.CompilePerms.CompilePermsOptionsImpl;
 import com.google.gwt.dev.CompileTaskRunner.CompileTask;
 import com.google.gwt.dev.Link.LinkOptionsImpl;
 import com.google.gwt.dev.Precompile.PrecompileOptionsImpl;
+import com.google.gwt.dev.cfg.ModuleDef;
+import com.google.gwt.dev.cfg.ModuleDefLoader;
 import com.google.gwt.dev.util.PerfLogger;
 import com.google.gwt.dev.util.Util;
 import com.google.gwt.dev.util.arg.ArgHandlerExtraDir;
+import com.google.gwt.dev.util.arg.ArgHandlerLocalWorkers;
 import com.google.gwt.dev.util.arg.ArgHandlerOutDir;
 import com.google.gwt.dev.util.arg.ArgHandlerWorkDirOptional;
 import com.google.gwt.util.tools.Utility;
@@ -44,6 +46,7 @@
       registerHandler(new ArgHandlerWorkDirOptional(options));
 
       registerHandler(new ArgHandlerExtraDir(options));
+      registerHandler(new ArgHandlerLocalWorkers(options));
       registerHandler(new ArgHandlerOutDir(options));
     }
 
@@ -57,6 +60,7 @@
       CompilerOptions {
 
     private LinkOptionsImpl linkOptions = new LinkOptionsImpl();
+    private int localWorkers;
 
     public GWTCompilerOptionsImpl() {
     }
@@ -68,12 +72,17 @@
     public void copyFrom(CompilerOptions other) {
       super.copyFrom(other);
       linkOptions.copyFrom(other);
+      localWorkers = other.getLocalWorkers();
     }
 
     public File getExtraDir() {
       return linkOptions.getExtraDir();
     }
 
+    public int getLocalWorkers() {
+      return localWorkers;
+    }
+
     public File getOutDir() {
       return linkOptions.getOutDir();
     }
@@ -82,6 +91,10 @@
       linkOptions.setExtraDir(extraDir);
     }
 
+    public void setLocalWorkers(int localWorkers) {
+      this.localWorkers = localWorkers;
+    }
+
     public void setOutDir(File outDir) {
       linkOptions.setOutDir(outDir);
     }
@@ -117,10 +130,15 @@
   }
 
   public boolean run(TreeLogger logger) throws UnableToCompleteException {
+    ModuleDef module = ModuleDefLoader.loadFromClassPath(logger,
+        options.getModuleName());
+
     if (options.isValidateOnly()) {
-      return new Precompile(options).run(logger);
+      return Precompile.validate(logger, options, module, options.getGenDir(),
+          options.getCompilerWorkDir());
     } else {
       PerfLogger.start("compile");
+      long compileStart = System.currentTimeMillis();
       logger = logger.branch(TreeLogger.INFO, "Compiling module "
           + options.getModuleName());
 
@@ -131,22 +149,24 @@
           tempWorkDir = true;
         }
 
-        if (new Precompile(options).run(logger)) {
-          /*
-           * TODO: use the in-memory result of Precompile to run CompilePerms
-           * instead of serializing through the file system.
-           */
-          CompilePermsOptionsImpl permsOptions = new CompilePermsOptionsImpl();
-          permsOptions.copyFrom(options);
-          if (new CompilePerms(permsOptions).run(logger)) {
-            if (new Link(options).run(logger)) {
-              logger.log(TreeLogger.INFO, "Compilation succeeded");
-              return true;
-            }
-          }
-        }
+        Precompilation precompilation = Precompile.precompile(logger, options,
+            module, options.getGenDir(), options.getCompilerWorkDir());
 
-        logger.log(TreeLogger.ERROR, "Compilation failed");
+        Permutation[] allPerms = precompilation.getPermutations();
+        File[] resultFiles = CompilePerms.makeResultFiles(
+            options.getCompilerWorkDir(), allPerms);
+        CompilePerms.compile(logger, precompilation, allPerms,
+            options.getLocalWorkers(), resultFiles);
+
+        Link.link(logger.branch(TreeLogger.INFO, "Linking into "
+            + options.getOutDir().getPath()), module, precompilation,
+            resultFiles, options.getOutDir(), options.getExtraDir());
+
+        long compileDone = System.currentTimeMillis();
+        long delta = compileDone - compileStart;
+        logger.log(TreeLogger.INFO, "Compilation succeeded -- "
+            + String.format("%.3f", delta / 1000d) + "s");
+        return true;
       } catch (IOException e) {
         logger.log(TreeLogger.ERROR,
             "Unable to create compiler work directory", e);
diff --git a/dev/core/src/com/google/gwt/dev/Link.java b/dev/core/src/com/google/gwt/dev/Link.java
index d476bbc..f10d910 100644
--- a/dev/core/src/com/google/gwt/dev/Link.java
+++ b/dev/core/src/com/google/gwt/dev/Link.java
@@ -100,20 +100,20 @@
   }
 
   public static ArtifactSet link(TreeLogger logger, ModuleDef module,
-      Precompilation precompilation, File[] jsFiles)
+      Precompilation precompilation, File[] resultFiles)
       throws UnableToCompleteException {
     StandardLinkerContext linkerContext = new StandardLinkerContext(logger,
         module, precompilation.getUnifiedAst().getOptions());
-    return doLink(logger, linkerContext, precompilation, jsFiles);
+    return doLink(logger, linkerContext, precompilation, resultFiles);
   }
 
   public static void link(TreeLogger logger, ModuleDef module,
-      Precompilation precompilation, File[] jsFiles, File outDir, File extrasDir)
-      throws UnableToCompleteException {
+      Precompilation precompilation, File[] resultFiles, File outDir,
+      File extrasDir) throws UnableToCompleteException {
     StandardLinkerContext linkerContext = new StandardLinkerContext(logger,
         module, precompilation.getUnifiedAst().getOptions());
     ArtifactSet artifacts = doLink(logger, linkerContext, precompilation,
-        jsFiles);
+        resultFiles);
     doProduceOutput(logger, artifacts, linkerContext, module, outDir, extrasDir);
   }
 
@@ -142,15 +142,15 @@
 
   private static ArtifactSet doLink(TreeLogger logger,
       StandardLinkerContext linkerContext, Precompilation precompilation,
-      File[] jsFiles) throws UnableToCompleteException {
+      File[] resultFiles) throws UnableToCompleteException {
     Permutation[] perms = precompilation.getPermutations();
-    if (perms.length != jsFiles.length) {
+    if (perms.length != resultFiles.length) {
       throw new IllegalArgumentException(
-          "Mismatched jsFiles.length and permutation count");
+          "Mismatched resultFiles.length and permutation count");
     }
 
     for (int i = 0; i < perms.length; ++i) {
-      finishPermuation(logger, perms[i], jsFiles[i], linkerContext);
+      finishPermuation(logger, perms[i], resultFiles[i], linkerContext);
     }
 
     linkerContext.addOrReplaceArtifacts(precompilation.getGeneratedArtifacts());
@@ -254,11 +254,11 @@
       return false;
     }
     Permutation[] perms = precompilation.getPermutations();
-    File[] jsFiles = new File[perms.length];
+    File[] resultFiles = new File[perms.length];
     for (int i = 0; i < perms.length; ++i) {
-      jsFiles[i] = CompilePerms.makePermFilename(options.getCompilerWorkDir(),
-          i);
-      if (!jsFiles[i].exists()) {
+      resultFiles[i] = CompilePerms.makePermFilename(
+          options.getCompilerWorkDir(), i);
+      if (!resultFiles[i].exists()) {
         logger.log(TreeLogger.ERROR, "File not found '"
             + precompilationFile.getAbsolutePath()
             + "'; please compile all permutations");
@@ -271,7 +271,7 @@
     StandardLinkerContext linkerContext = new StandardLinkerContext(branch,
         module, precompilation.getUnifiedAst().getOptions());
     ArtifactSet artifacts = doLink(branch, linkerContext, precompilation,
-        jsFiles);
+        resultFiles);
 
     doProduceOutput(branch, artifacts, linkerContext, module,
         options.getOutDir(), options.getExtraDir());
diff --git a/dev/core/src/com/google/gwt/dev/OptionLocalWorkers.java b/dev/core/src/com/google/gwt/dev/OptionLocalWorkers.java
new file mode 100644
index 0000000..e853888
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/OptionLocalWorkers.java
@@ -0,0 +1,31 @@
+/*
+ * 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;
+
+/**
+ * Controls the number of local PermutationWorkers to be used.
+ */
+public interface OptionLocalWorkers {
+  /**
+   * Returns the number of workers to run on this machine.
+   */
+  int getLocalWorkers();
+
+  /**
+   * Sets the number of workers to run on this machine.
+   */
+  void setLocalWorkers(int localWorkers);
+}
diff --git a/dev/core/src/com/google/gwt/dev/Permutation.java b/dev/core/src/com/google/gwt/dev/Permutation.java
index 13ca881..3eba3f2 100644
--- a/dev/core/src/com/google/gwt/dev/Permutation.java
+++ b/dev/core/src/com/google/gwt/dev/Permutation.java
@@ -28,13 +28,19 @@
  * Represents the state of a single permutation for compile.
  */
 public final class Permutation implements Serializable {
+  private final int id;
   private final List<StaticPropertyOracle> propertyOracles = new ArrayList<StaticPropertyOracle>();
   private final SortedMap<String, String> rebindAnswers = new TreeMap<String, String>();
 
-  public Permutation(StaticPropertyOracle propertyOracle) {
+  public Permutation(int id, StaticPropertyOracle propertyOracle) {
+    this.id = id;
     this.propertyOracles.add(propertyOracle);
   }
 
+  public int getId() {
+    return id;
+  }
+
   public StaticPropertyOracle[] getPropertyOracles() {
     return propertyOracles.toArray(new StaticPropertyOracle[propertyOracles.size()]);
   }
diff --git a/dev/core/src/com/google/gwt/dev/PermutationResult.java b/dev/core/src/com/google/gwt/dev/PermutationResult.java
new file mode 100644
index 0000000..0b6f1bf
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/PermutationResult.java
@@ -0,0 +1,28 @@
+/*
+ * 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 java.io.Serializable;
+
+/**
+ * An extensible return type for the results of compiling a single permutation.
+ */
+public interface PermutationResult extends Serializable {
+  /**
+   * The compiled JavaScript code.
+   */
+  String[] getJs();
+}
diff --git a/dev/core/src/com/google/gwt/dev/PermutationWorker.java b/dev/core/src/com/google/gwt/dev/PermutationWorker.java
new file mode 100644
index 0000000..f24d35f
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/PermutationWorker.java
@@ -0,0 +1,50 @@
+/*
+ * 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;
+
+/**
+ * Represents a facility that can compile an individual {@link Permutation}.
+ * Instances of PermutationWorker are expected to be created via
+ * {@link PermutationWorkerFactory#getWorkers(int)}.
+ */
+interface PermutationWorker {
+
+  /**
+   * Compile a single permutation. The {@link com.google.gwt.dev.jjs.UnifiedAst}
+   * will have been provided to {@link PermutationWorkerFactory#getWorkers}
+   * method.
+   * 
+   * @throws TransientWorkerException if the Permutation should be tried again
+   *           on another worker
+   * @throws UnableToCompleteException due to a fatal error
+   */
+  PermutationResult compile(TreeLogger logger, Permutation permutation)
+      throws TransientWorkerException, UnableToCompleteException;
+
+  /**
+   * Returns a human-readable description of the worker instance. This may be
+   * used for error reporting.
+   */
+  String getName();
+
+  /**
+   * Release any resources associated with the worker.
+   */
+  void shutdown();
+}
diff --git a/dev/core/src/com/google/gwt/dev/PermutationWorkerFactory.java b/dev/core/src/com/google/gwt/dev/PermutationWorkerFactory.java
new file mode 100644
index 0000000..4448c3d
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/PermutationWorkerFactory.java
@@ -0,0 +1,396 @@
+/*
+ * 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.dev.jjs.InternalCompilerException;
+import com.google.gwt.dev.jjs.UnifiedAst;
+import com.google.gwt.dev.util.Util;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+/**
+ * Represents a factory for implementations of an endpoint that will invoke
+ * CompilePerms. Implementations of PermutationWorkerFactory should be
+ * default-instantiable and will have {@link #init} called immediately after
+ * construction.
+ */
+public abstract class PermutationWorkerFactory {
+
+  /**
+   * This callable is responsible for compiling one permutation. It waits for an
+   * available worker, uses it, and returns it to the pool when finished.
+   */
+  private static class CompileOnePermutation implements Callable<ResultStatus> {
+    private final BlockingQueue<PermutationWorker> availableWorkers;
+    private final TreeLogger logger;
+    private final Permutation permutation;
+    private final File resultFile;
+
+    public CompileOnePermutation(TreeLogger logger, Permutation permutation,
+        File resultFile, BlockingQueue<PermutationWorker> availableWorkers) {
+      this.logger = logger;
+      this.permutation = permutation;
+      this.resultFile = resultFile;
+      this.availableWorkers = availableWorkers;
+    }
+
+    public ResultStatus call() {
+      // Find a free worker
+      PermutationWorker worker;
+      try {
+        worker = availableWorkers.take();
+      } catch (InterruptedException e) {
+        logger.log(TreeLogger.DEBUG, "Worker interrupted", e);
+        return ResultStatus.HARD_FAILURE;
+      }
+
+      if (worker == noMoreWorkersWorker) {
+        // Shutting down
+        return ResultStatus.HARD_FAILURE;
+      }
+
+      // Invoke the worker
+      try {
+        PermutationResult result = worker.compile(logger, permutation);
+        Util.writeObjectAsFile(logger, resultFile, result);
+        logger.log(TreeLogger.DEBUG, "Successfully compiled permutation");
+        availableWorkers.add(worker);
+        return ResultStatus.SUCCESS;
+      } catch (TransientWorkerException e) {
+        logger.log(TreeLogger.DEBUG, "Worker died, will retry Permutation", e);
+        return ResultStatus.TRANSIENT_FAILURE;
+      } catch (UnableToCompleteException e) {
+        logger.log(TreeLogger.ERROR, "Unrecoverable exception, shutting down",
+            e);
+        return ResultStatus.HARD_FAILURE;
+      }
+    }
+  }
+
+  private static enum ResultStatus {
+    /**
+     * A failure bad enough to merit shutting down the compilation.
+     */
+    HARD_FAILURE,
+
+    /**
+     * A successful compile.
+     */
+    SUCCESS,
+
+    /**
+     * A worker died while processing this permutation, but it is worth trying
+     * to compile it with another worker.
+     */
+    TRANSIENT_FAILURE
+  };
+
+  /**
+   * The name of the system property used to define the workers.
+   */
+  public static final String FACTORY_IMPL_PROPERTY = "gwt.jjs.permutationWorkerFactory";
+
+  /**
+   * This value can be passed into {@link #setLocalWorkers(int)} to indicate
+   * that a heuristic should be used to determine the total number of local
+   * workers.
+   */
+  public static final int WORKERS_AUTO = 0;
+
+  private static List<PermutationWorkerFactory> lazyFactories;
+
+  private static final PermutationWorker noMoreWorkersWorker = new PermutationWorker() {
+
+    public PermutationResult compile(TreeLogger logger, Permutation permutation)
+        throws TransientWorkerException, UnableToCompleteException {
+      throw new UnableToCompleteException();
+    }
+
+    public String getName() {
+      return "Marker worker indicating no more workers";
+    }
+
+    public void shutdown() {
+    }
+  };
+
+  /**
+   * Compiles all Permutations in a Precompilation and returns an array of Files
+   * that can be consumed by Link using the system-default
+   * PermutationWorkersFactories.
+   */
+  public static void compilePermutations(TreeLogger logger,
+      Precompilation precompilation, int localWorkers, File[] resultFiles)
+      throws UnableToCompleteException {
+    compilePermutations(logger, precompilation,
+        precompilation.getPermutations(), localWorkers, resultFiles);
+  }
+
+  /**
+   * Compiles a subset of the Permutations in a Precompilation and returns an
+   * array of Files that can be consumed by Link using the system-default
+   * PermutationWorkersFactories.
+   * 
+   * @param localWorkers Set the maximum number of workers that should be
+   *          executed on the local system by the PermutationWorkerFactory. The
+   *          value {@link #WORKERS_AUTO} will allow the
+   *          PermutationWorkerFactory to apply a heuristic to determine the
+   *          correct number of local workers.
+   * @param resultFiles the output files to write into; must be the same length
+   *          as permutations
+   */
+  public static void compilePermutations(TreeLogger logger,
+      Precompilation precompilation, Permutation[] permutations,
+      int localWorkers, File[] resultFiles) throws UnableToCompleteException {
+    assert permutations.length == resultFiles.length;
+    assert Arrays.asList(precompilation.getPermutations()).containsAll(
+        Arrays.asList(permutations));
+
+    // We may have a mixed collection of workers from different factories
+    List<PermutationWorker> workers = new ArrayList<PermutationWorker>();
+
+    /*
+     * We can have errors below this point, there's a finally block to handle
+     * cleanup of workers.
+     */
+    try {
+      createWorkers(logger, precompilation.getUnifiedAst(),
+          permutations.length, localWorkers, workers);
+      ExecutorService executor = Executors.newFixedThreadPool(workers.size());
+
+      // List of available workers.
+      // The extra space is for inserting nulls at shutdown time
+      BlockingQueue<PermutationWorker> availableWorkers = new ArrayBlockingQueue<PermutationWorker>(
+          2 * workers.size());
+      availableWorkers.addAll(workers);
+
+      try {
+
+        // Submit all tasks to the executor
+
+        // The permutation compiles not yet finished
+        Queue<CompileOnePermutation> tasksOutstanding = new LinkedList<CompileOnePermutation>();
+
+        // The futures for the results of those compiles
+        Queue<Future<ResultStatus>> resultFutures = new LinkedList<Future<ResultStatus>>();
+
+        for (int i = 0; i < permutations.length; ++i) {
+          TreeLogger permLogger = logger.branch(TreeLogger.DEBUG,
+              "Worker permutation " + permutations[i].getId() + " of "
+                  + permutations.length);
+          CompileOnePermutation task = new CompileOnePermutation(permLogger,
+              permutations[i], resultFiles[i], availableWorkers);
+          tasksOutstanding.add(task);
+          resultFutures.add(executor.submit(task));
+        }
+
+        // Count the number of dead workers
+        int numDeadWorkers = 0;
+        int successCount = 0;
+
+        while (!resultFutures.isEmpty() && numDeadWorkers < workers.size()) {
+          assert resultFutures.size() == tasksOutstanding.size();
+
+          CompileOnePermutation task = tasksOutstanding.remove();
+          Future<ResultStatus> future = resultFutures.remove();
+          ResultStatus result;
+          try {
+            result = future.get();
+          } catch (InterruptedException e) {
+            logger.log(TreeLogger.ERROR,
+                "Exiting without results due to interruption", e);
+            throw new UnableToCompleteException();
+          } catch (ExecutionException e) {
+            logger.log(TreeLogger.ERROR, "A compilation failed", e);
+            throw new UnableToCompleteException();
+          }
+
+          if (result == ResultStatus.SUCCESS) {
+            ++successCount;
+          } else if (result == ResultStatus.TRANSIENT_FAILURE) {
+            // A worker died. Resubmit for the remaining workers.
+            ++numDeadWorkers;
+            tasksOutstanding.add(task);
+            resultFutures.add(executor.submit(task));
+          } else if (result == ResultStatus.HARD_FAILURE) {
+            // Shut down.
+            break;
+          } else {
+            throw new InternalCompilerException("Unknown result type");
+          }
+        }
+
+        // Too many permutations is a coding error
+        assert successCount <= permutations.length;
+
+        if (successCount < permutations.length) {
+          // Likely as not, all of the workers died
+          logger.log(TreeLogger.ERROR, "Not all permutation were compiled "
+              + successCount + " of " + permutations.length);
+          throw new UnableToCompleteException();
+        }
+      } finally {
+        // Shut down the executor
+        executor.shutdown();
+
+        // Inform any residual CompileOnePermutation's that there aren't any
+        // more workers
+        for (int i = 0; i < workers.size(); i++) {
+          availableWorkers.add(noMoreWorkersWorker);
+        }
+      }
+    } finally {
+      // Shut down all workers
+      for (PermutationWorker worker : workers) {
+        worker.shutdown();
+      }
+    }
+  }
+
+  /**
+   * Creates one or more implementations of worker factories. This will treat
+   * the value of the {@value #FACTORY_IMPL_PROPERTY} system property as a
+   * comma-separated list of type names.
+   */
+  private static synchronized List<PermutationWorkerFactory> createAll(
+      TreeLogger logger) throws UnableToCompleteException {
+    // NB: This is the much-derided FactoryFactory pattern
+
+    logger = logger.branch(TreeLogger.TRACE,
+        "Creating PermutationWorkerFactory instances");
+
+    if (lazyFactories != null) {
+      logger.log(TreeLogger.SPAM, "Using lazy instances");
+      return lazyFactories;
+    }
+
+    List<PermutationWorkerFactory> mutableFactories = new ArrayList<PermutationWorkerFactory>();
+    String classes = System.getProperty(FACTORY_IMPL_PROPERTY,
+        ThreadedPermutationWorkerFactory.class.getName() + ","
+            + ExternalPermutationWorkerFactory.class.getName());
+    logger.log(TreeLogger.SPAM, "Factory impl property is " + classes);
+
+    String[] classParts = classes.split(",");
+    for (String className : classParts) {
+      try {
+        Class<? extends PermutationWorkerFactory> clazz = Class.forName(
+            className).asSubclass(PermutationWorkerFactory.class);
+        PermutationWorkerFactory factory = clazz.newInstance();
+        factory.init(logger);
+        mutableFactories.add(factory);
+        logger.log(TreeLogger.SPAM, "Added PermutationWorkerFactory "
+            + clazz.getName());
+      } catch (ClassCastException e) {
+        logger.log(TreeLogger.ERROR, className + " is not a "
+            + PermutationWorkerFactory.class.getName());
+      } catch (ClassNotFoundException e) {
+        logger.log(TreeLogger.ERROR,
+            "Unable to find PermutationWorkerFactory named " + className);
+      } catch (InstantiationException e) {
+        logger.log(TreeLogger.ERROR,
+            "Unable to instantiate PermutationWorkerFactory " + className, e);
+      } catch (IllegalAccessException e) {
+        logger.log(TreeLogger.ERROR,
+            "Unable to instantiate PermutationWorkerFactory " + className, e);
+      }
+    }
+
+    if (mutableFactories.size() == 0) {
+      logger.log(TreeLogger.ERROR,
+          "No usable PermutationWorkerFactories available");
+      throw new UnableToCompleteException();
+    }
+
+    return lazyFactories = Collections.unmodifiableList(mutableFactories);
+  }
+
+  /**
+   * Create as many workers as possible to service the Permutations.
+   */
+  private static void createWorkers(TreeLogger logger, UnifiedAst unifiedAst,
+      int workersNeeded, int localWorkers, List<PermutationWorker> workers)
+      throws UnableToCompleteException {
+    if (localWorkers <= WORKERS_AUTO) {
+      // TODO: something smarter?
+      localWorkers = 1;
+    }
+
+    for (PermutationWorkerFactory factory : PermutationWorkerFactory.createAll(logger)) {
+      if (workersNeeded <= 0) {
+        break;
+      }
+
+      int wanted = factory.isLocal() ? Math.min(workersNeeded, localWorkers)
+          : workersNeeded;
+      if (wanted <= 0) {
+        continue;
+      }
+
+      Collection<PermutationWorker> newWorkers = factory.getWorkers(logger,
+          unifiedAst, wanted);
+
+      workers.addAll(newWorkers);
+      workersNeeded -= newWorkers.size();
+      if (factory.isLocal()) {
+        localWorkers -= newWorkers.size();
+      }
+    }
+
+    if (workers.size() == 0) {
+      logger.log(TreeLogger.ERROR, "No PermutationWorkers created");
+      throw new UnableToCompleteException();
+    }
+  }
+
+  /**
+   * Return some number of PermutationWorkers.
+   * 
+   * @param unifiedAst a UnifiedAst
+   * @param numWorkers the desired number of workers
+   * @return a collection of PermutationWorkers, the size of which may be less
+   *         than <code>numWorkers</code>
+   */
+  public abstract Collection<PermutationWorker> getWorkers(TreeLogger logger,
+      UnifiedAst unifiedAst, int numWorkers) throws UnableToCompleteException;
+
+  /**
+   * Initialize the PermutationWorkerFactory.
+   */
+  public abstract void init(TreeLogger logger) throws UnableToCompleteException;
+
+  /**
+   * Indicates if the PermutationWorkers created by the factory consume
+   * computational or memory resources on the local system, as opposed to the
+   * per-permutation work being performed on a remote system.
+   */
+  public abstract boolean isLocal();
+}
diff --git a/dev/core/src/com/google/gwt/dev/Precompile.java b/dev/core/src/com/google/gwt/dev/Precompile.java
index a7e8bf0..c5a4d82 100644
--- a/dev/core/src/com/google/gwt/dev/Precompile.java
+++ b/dev/core/src/com/google/gwt/dev/Precompile.java
@@ -171,7 +171,7 @@
         rebindOracles[i] = new StandardRebindOracle(compilationState,
             propertyOracles[i], module, rules, genDir, generatorResourcesDir,
             generatorArtifacts);
-        permutations[i] = new Permutation(propertyOracles[i]);
+        permutations[i] = new Permutation(i, propertyOracles[i]);
       }
     }
 
diff --git a/dev/core/src/com/google/gwt/dev/ThreadedPermutationWorkerFactory.java b/dev/core/src/com/google/gwt/dev/ThreadedPermutationWorkerFactory.java
new file mode 100644
index 0000000..89eebf0
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/ThreadedPermutationWorkerFactory.java
@@ -0,0 +1,103 @@
+/*
+ * 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.dev.jjs.UnifiedAst;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Create a single in-process PermutationWorker. This WorkerFactory is intended
+ * to be used as a fall-back in case the other PermutationWorkers are unable to
+ * complete.
+ */
+public class ThreadedPermutationWorkerFactory extends PermutationWorkerFactory {
+
+  private static class ThreadedPermutationWorker implements PermutationWorker {
+    private final UnifiedAst ast;
+    private final int id;
+
+    public ThreadedPermutationWorker(UnifiedAst ast, int id) {
+      this.ast = ast;
+      this.id = id;
+    }
+
+    public PermutationResult compile(final TreeLogger logger,
+        final Permutation permutation) throws TransientWorkerException,
+        UnableToCompleteException {
+      try {
+        return CompilePerms.compile(logger, permutation, ast);
+      } catch (OutOfMemoryError e) {
+        logger.log(TreeLogger.ERROR,
+            "OutOfMemoryError: Increase heap size or lower "
+                + MAX_THREADS_PROPERTY);
+        throw new UnableToCompleteException();
+      }
+    }
+
+    public String getName() {
+      return "In-process PermutationWorker " + id;
+    }
+
+    public void shutdown() {
+      // No-op
+    }
+  }
+
+  /**
+   * A Java system property that can be used to change the number of in-process
+   * threads used.
+   */
+  public static final String MAX_THREADS_PROPERTY = "gwt.jjs.maxThreads";
+
+  @Override
+  public Collection<PermutationWorker> getWorkers(TreeLogger logger,
+      UnifiedAst unifiedAst, int numWorkers) throws UnableToCompleteException {
+    logger = logger.branch(TreeLogger.SPAM,
+        "Creating ThreadedPermutationWorkers");
+
+    numWorkers = Math.min(numWorkers, Integer.getInteger(MAX_THREADS_PROPERTY,
+        1));
+
+    if (numWorkers == 0) {
+      return Collections.emptyList();
+    }
+
+    // The worker will deserialize a new copy
+    List<PermutationWorker> toReturn = new ArrayList<PermutationWorker>(
+        numWorkers);
+    for (int i = 0; i < numWorkers; i++) {
+      toReturn.add(new ThreadedPermutationWorker(unifiedAst, i));
+    }
+    return toReturn;
+  }
+
+  @Override
+  public void init(TreeLogger logger) throws UnableToCompleteException {
+    logger = logger.branch(TreeLogger.SPAM,
+        "Initializing ThreadedPermutationWorkerFactory");
+  }
+
+  @Override
+  public boolean isLocal() {
+    return true;
+  }
+}
diff --git a/dev/core/src/com/google/gwt/dev/TransientWorkerException.java b/dev/core/src/com/google/gwt/dev/TransientWorkerException.java
new file mode 100644
index 0000000..5f4e2bc
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/TransientWorkerException.java
@@ -0,0 +1,28 @@
+/*
+ * 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;
+
+/**
+ * Indicates that a PermutationWorker failed to complete its assignment, but
+ * that the permutation should be retried on another worker. This would be used
+ * to report errors such as OutOfMemory when compiling in-process or unexpected
+ * death of an out-of-process worker (network break, etc).
+ */
+public class TransientWorkerException extends Exception {
+  public TransientWorkerException(String msg, Throwable cause) {
+    super(msg, cause);
+  }
+}
diff --git a/dev/core/src/com/google/gwt/dev/jjs/UnifiedAst.java b/dev/core/src/com/google/gwt/dev/jjs/UnifiedAst.java
index 6dfc5fe..d0d2e6d 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/UnifiedAst.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/UnifiedAst.java
@@ -143,6 +143,18 @@
     return rebindRequests;
   }
 
+  /**
+   * Internally prepares a new AST for compilation if one is not already
+   * prepared.
+   */
+  public void prepare() {
+    synchronized (myLockObject) {
+      if (initialAst == null) {
+        initialAst = deserializeAst(serializedAst);
+      }
+    }
+  }
+
   AST getFreshAst() {
     synchronized (myLockObject) {
       if (initialAst != null) {
diff --git a/dev/core/src/com/google/gwt/dev/util/Util.java b/dev/core/src/com/google/gwt/dev/util/Util.java
index 31f6972..53e5f0c 100644
--- a/dev/core/src/com/google/gwt/dev/util/Util.java
+++ b/dev/core/src/com/google/gwt/dev/util/Util.java
@@ -311,8 +311,7 @@
   }
 
   /**
-   * Returns a String as a byte-array using the default encoding for the
-   * compiler.
+   * Returns a byte-array representing the default encoding for a String.
    */
   public static byte[] getBytes(String s) {
     try {
@@ -323,6 +322,10 @@
     }
   }
 
+  /**
+   * Returns an array of byte-arrays representing the default encoding for an
+   * array of Strings.
+   */
   public static byte[][] getBytes(String[] s) {
     byte[][] bytes = new byte[s.length][];
     for (int i = 0; i < s.length; i++) {
@@ -578,24 +581,24 @@
     logger.log(TreeLogger.INFO, "Unable to dump source to disk", caught);
   }
 
-  public static byte[][] readFileAndSplit(File file) {
-    RandomAccessFile f = null;
-    try {
-      f = new RandomAccessFile(file, "r");
-      int length = f.readInt();
-      byte[][] results = new byte[length][];
-      for (int i = 0; i < length; i++) {
-        int nextLength = f.readInt();
-        results[i] = new byte[nextLength];
-        f.read(results[i]);
-      }
-      return results;
-    } catch (IOException e) {
-      return null;
-    } finally {
-      Utility.close(f);
-    }
-  }
+  // public static byte[][] readFileAndSplit(File file) {
+  // RandomAccessFile f = null;
+  // try {
+  // f = new RandomAccessFile(file, "r");
+  // int length = f.readInt();
+  // byte[][] results = new byte[length][];
+  // for (int i = 0; i < length; i++) {
+  // int nextLength = f.readInt();
+  // results[i] = new byte[nextLength];
+  // f.read(results[i]);
+  // }
+  // return results;
+  // } catch (IOException e) {
+  // return null;
+  // } finally {
+  // Utility.close(f);
+  // }
+  // }
 
   public static byte[] readFileAsBytes(File file) {
     FileInputStream fileInputStream = null;
@@ -1155,30 +1158,31 @@
     }
   }
 
-  /**
-   * Write all of the supplied bytes to the file, in a way that they can be read
-   * back by {@link #readFileAndSplit(File).
-   */
-  public static boolean writeStringsAsFile(TreeLogger branch,
-      File makePermFilename, String[] js) {
-    RandomAccessFile f = null;
-    try {
-      makePermFilename.delete();
-      makePermFilename.getParentFile().mkdirs();
-      f = new RandomAccessFile(makePermFilename, "rwd");
-      f.writeInt(js.length);
-      for (String s : js) {
-        byte[] b = getBytes(s);
-        f.writeInt(b.length);
-        f.write(b);
-      }
-      return true;
-    } catch (IOException e) {
-      return false;
-    } finally {
-      Utility.close(f);
-    }
-  }
+  // /**
+  // * Write all of the supplied bytes to the file, in a way that they can be
+  // read
+  // * back by {@link #readFileAndSplit(File).
+  // */
+  // public static boolean writeStringsAsFile(TreeLogger branch,
+  // File makePermFilename, String[] js) {
+  // RandomAccessFile f = null;
+  // try {
+  // makePermFilename.delete();
+  // makePermFilename.getParentFile().mkdirs();
+  // f = new RandomAccessFile(makePermFilename, "rwd");
+  // f.writeInt(js.length);
+  // for (String s : js) {
+  // byte[] b = getBytes(s);
+  // f.writeInt(b.length);
+  // f.write(b);
+  // }
+  // return true;
+  // } catch (IOException e) {
+  // return false;
+  // } finally {
+  // Utility.close(f);
+  // }
+  // }
 
   /**
    * Reads the specified number of bytes from the {@link InputStream}.
diff --git a/dev/core/src/com/google/gwt/dev/util/arg/ArgHandlerLocalWorkers.java b/dev/core/src/com/google/gwt/dev/util/arg/ArgHandlerLocalWorkers.java
new file mode 100644
index 0000000..6800c83
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/util/arg/ArgHandlerLocalWorkers.java
@@ -0,0 +1,58 @@
+/*
+ * 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.util.arg;
+
+import com.google.gwt.dev.OptionLocalWorkers;
+import com.google.gwt.util.tools.ArgHandlerInt;
+
+/**
+ * An arg handler to specify the number of local workers that should be used by
+ * the compiler.
+ */
+public class ArgHandlerLocalWorkers extends ArgHandlerInt {
+
+  private final OptionLocalWorkers options;
+
+  public ArgHandlerLocalWorkers(OptionLocalWorkers options) {
+    this.options = options;
+  }
+
+  @Override
+  public String[] getDefaultArgs() {
+    // Default to 1 for now; we might do an "auto" later.
+    return new String[] {getTag(), "1"};
+  }
+
+  @Override
+  public String getPurpose() {
+    return "Specifies the number of local workers to use whe compiling permutations";
+  }
+
+  @Override
+  public String getTag() {
+    return "-localWorkers";
+  }
+
+  @Override
+  public String[] getTagArgs() {
+    return new String[] {"count"};
+  }
+
+  @Override
+  public void setInt(int value) {
+    options.setLocalWorkers(value);
+  }
+}
diff --git a/reference/dispatch/Dispatch.gwt.xml b/reference/dispatch/Dispatch.gwt.xml
new file mode 100644
index 0000000..fa1c708
--- /dev/null
+++ b/reference/dispatch/Dispatch.gwt.xml
@@ -0,0 +1,6 @@
+<!DOCTYPE document SYSTEM
+    "http://google-web-toolkit.googlecode.com/svn/releases/1.5/distro-source/core/src/gwt-module.dtd">
+<module>
+  <inherits name='com.google.gwt.user.User' />
+  <entry-point class='kellegous.client.Dispatch' />
+</module>
diff --git a/reference/dispatch/client/Dispatch.java b/reference/dispatch/client/Dispatch.java
new file mode 100644
index 0000000..cac6f18
--- /dev/null
+++ b/reference/dispatch/client/Dispatch.java
@@ -0,0 +1,143 @@
+package kellegous.client;
+
+import com.google.gwt.core.client.Duration;
+import com.google.gwt.core.client.EntryPoint;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.EventListener;
+
+public class Dispatch implements EntryPoint {
+  private static native Event createMockEvent() /*-{
+    return { type: "click" };
+  }-*/;
+
+  private static double[] runTrial(int numberOfObservers,
+      int numberOfIterations, int numberOfSamples) {
+    final double[] results = new double[numberOfSamples];
+    final EventListener subject = Subject.create(numberOfObservers);
+    final Event event = createMockEvent();
+    for (int j = 0; j < numberOfSamples; ++j) {
+      final Duration d = new Duration();
+      for (int i = 0; i < numberOfIterations; ++i) {
+        subject.onBrowserEvent(event);
+      }
+      results[j] = d.elapsedMillis();
+    }
+    return results;
+  }
+
+  private static native void schedule(Command command) /*-{
+    $wnd.setTimeout(function() {
+      command.@com.google.gwt.user.client.Command::execute()();
+    }, 0);
+  }-*/;
+
+  // t-distribution for p = 0.05 (used to compute 95% confidence intervals).
+  // This table is based at df = 2.
+  private final static double[] TDIST = new double[] {
+      4.3027, 3.1824, 2.7765, 2.5706, 2.4469, 2.3646, 2.3060, 2.2622, 2.2281,
+      2.2010, 2.1788, 2.1604, 2.1448, 2.1315, 2.1199, 2.1098, 2.1009, 2.0930,
+      2.0860, 2.0796, 2.0739, 2.0687, 2.0639, 2.0595, 2.0555, 2.0518, 2.0484,
+      2.0452, 2.0423, 2.0395, 2.0369, 2.0345, 2.0322, 2.0301, 2.0281, 2.0262,
+      2.0244, 2.0227, 2.0211, 2.0195, 2.0181, 2.0167, 2.0154, 2.0141, 2.0129,
+      2.0117, 2.0106, 2.0096, 2.0086, 2.0076, 2.0066, 2.0057, 2.0049, 2.0040,
+      2.0032, 2.0025, 2.0017, 2.0010, 2.0003, 1.9996, 1.9990, 1.9983, 1.9977,
+      1.9971, 1.9966, 1.9960, 1.9955, 1.9949, 1.9944, 1.9939, 1.9935, 1.9930,
+      1.9925, 1.9921, 1.9917, 1.9913, 1.9908, 1.9905, 1.9901, 1.9897, 1.9893,
+      1.9890, 1.9886, 1.9883, 1.9879, 1.9876, 1.9873, 1.9870, 1.9867, 1.9864,
+      1.9861, 1.9858, 1.9855, 1.9852, 1.9850, 1.9847, 1.9845, 1.9842, 1.9840};
+
+  private static double computeT(int df) {
+    return TDIST[df - 2];
+  }
+
+  private static double computeMean(double[] s) {
+    double sum = 0.0;
+    final int n = s.length;
+    for (int i = 0; i < n; ++i) {
+      sum += s[i];
+    }
+    return sum / n;
+  }
+
+  private static double computeStandardError(double[] data, double mean) {
+    final int n = data.length;
+    double sum = 0.0;
+    for (int i = 0; i < n; ++i) {
+      final double d = data[i] - mean;
+      sum += d * d;
+    }
+
+    return Math.sqrt(sum / n) / Math.sqrt(n);
+  }
+
+  private static class Stats {
+    private final double mean, upper, lower;
+
+    Stats(double[] data) {
+      mean = computeMean(data);
+      final double error = computeStandardError(data, mean);
+      final double t = computeT(data.length - 1);
+      upper = mean + t * error;
+      lower = mean - t * error;
+    }
+
+    @Override
+    public String toString() {
+      return mean + ", " + lower + ", " + upper;
+    }
+  }
+
+  public static class Runner {
+    private int n;
+    private final int max, numIterations, numSamples;
+    private Stats[] results;
+
+    Runner(int min, int max, int numIterations, int numSamples) {
+      n = min;
+      this.max = max;
+      this.numIterations = numIterations;
+      this.numSamples = numSamples;
+      results = new Stats[max - min + 1];
+    }
+
+    void finish() {
+      final Document document = Document.get();
+      final DivElement root = document.createDivElement();
+      document.getBody().appendChild(root);
+      for (int i = 0, n = results.length; i < n; ++i) {
+        final DivElement div = document.createDivElement();
+        root.appendChild(div);
+        div.setInnerText("" + results[i].toString());
+      }
+    }
+
+    void next() {
+      schedule(new Command() {
+        public void execute() {
+          final double[] results = runTrial(n, numIterations, numSamples);
+          Runner.this.results[n] = new Stats(results);
+          if (++n <= max) {
+            next();
+          } else {
+            finish();
+          }
+        }
+      });
+    }
+
+    void run() {
+      next();
+    }
+  }
+
+  public void onModuleLoad() {
+    // Don't run this in hosted mode.
+    if (GWT.isScript()) {
+      new Runner(0, 10, 10000, 20).run();
+    }
+  }
+}
diff --git a/reference/dispatch/client/Subject.java b/reference/dispatch/client/Subject.java
new file mode 100644
index 0000000..fc21bee
--- /dev/null
+++ b/reference/dispatch/client/Subject.java
@@ -0,0 +1,22 @@
+package kellegous.client;
+
+import com.google.gwt.user.client.EventListener;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.ClickListener;
+import com.google.gwt.user.client.ui.Widget;
+
+public class Subject {
+  public static EventListener create(int numberOfObserver) {
+    final Button subject = new Button("a button");
+    for (int i = 0; i < numberOfObserver; ++i) {
+      subject.addClickListener(new ClickListener() {
+        private int count = 0;
+
+        public void onClick(Widget sender) {
+          count++;
+        }
+      });
+    }
+    return subject;
+  }
+}
diff --git a/reference/dispatch/public/Dispatch.html b/reference/dispatch/public/Dispatch.html
new file mode 100644
index 0000000..e87c244
--- /dev/null
+++ b/reference/dispatch/public/Dispatch.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+  <head>
+    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+    <title>Dispatch5</title>
+    <script type="text/javascript" language="javascript" src="kellegous.Dispatch.nocache.js"></script>
+  </head>
+  <body>
+  </body>
+</html>
diff --git a/samples/showcase/src/com/google/gwt/sample/showcase/client/ContentWidget.java b/samples/showcase/src/com/google/gwt/sample/showcase/client/ContentWidget.java
index 9d656ad..a4378e3 100644
--- a/samples/showcase/src/com/google/gwt/sample/showcase/client/ContentWidget.java
+++ b/samples/showcase/src/com/google/gwt/sample/showcase/client/ContentWidget.java
@@ -28,9 +28,9 @@
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.DeckPanel;
 import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.LazyPanel;
 import com.google.gwt.user.client.ui.TabBar;
 import com.google.gwt.user.client.ui.VerticalPanel;
 import com.google.gwt.user.client.ui.Widget;
@@ -59,7 +59,7 @@
  * <li>.sc-ContentWidget-description { Applied to the description }</li>
  * </ul>
  */
-public abstract class ContentWidget extends Composite implements
+public abstract class ContentWidget extends LazyPanel implements
     SelectionHandler<Integer> {
   /**
    * The constants used in this Content Widget.
@@ -90,7 +90,7 @@
   /**
    * An instance of the constants.
    */
-  private CwConstants constants;
+  private final CwConstants constants;
 
   /**
    * The deck panel with the contents.
@@ -98,11 +98,6 @@
   private DeckPanel deckPanel = null;
 
   /**
-   * A boolean indicating whether or not this widget has been initialized.
-   */
-  private boolean initialized = false;
-
-  /**
    * A boolean indicating whether or not the RPC request for the source code has
    * been sent.
    */
@@ -131,13 +126,11 @@
   public ContentWidget(CwConstants constants) {
     this.constants = constants;
     tabBar = new TabBar();
-    deckPanel = new DeckPanel();
-    initWidget(deckPanel);
-    setStyleName(DEFAULT_STYLE_NAME);
   }
 
   /**
-   * Add an item to this content widget.
+   * Add an item to this content widget. Should not be called before
+   * {@link #onInitializeComplete} has been called.
    * 
    * @param w the widget to add
    * @param tabText the text to display in the tab
@@ -187,66 +180,8 @@
   }
 
   /**
-   * Initialize this widget by creating the elements that should be added to the
-   * page.
-   */
-  public final void initialize() {
-    if (initialized == false) {
-      initialized = true;
-
-      // Add a tab handler
-      tabBar.addSelectionHandler(this);
-
-      // Create a container for the main example
-      final VerticalPanel vPanel = new VerticalPanel();
-      add(vPanel, constants.contentWidgetExample());
-
-      // Add the name
-      HTML nameWidget = new HTML(getName());
-      nameWidget.setStyleName(DEFAULT_STYLE_NAME + "-name");
-      vPanel.add(nameWidget);
-
-      // Add the description
-      HTML descWidget = new HTML(getDescription());
-      descWidget.setStyleName(DEFAULT_STYLE_NAME + "-description");
-      vPanel.add(descWidget);
-
-      // Add source code tab
-      if (hasSource()) {
-        sourceWidget = new HTML();
-        add(sourceWidget, constants.contentWidgetSource());
-      } else {
-        sourceLoaded = true;
-      }
-
-      // Add style tab
-      if (hasStyle()) {
-        styleDefs = new HashMap<String, String>();
-        styleWidget = new HTML();
-        add(styleWidget, constants.contentWidgetStyle());
-      }
-
-      asyncOnInitialize(new AsyncCallback<Widget>() {
-
-        public void onFailure(Throwable caught) {
-          Window.alert("exception: " + caught);
-        }
-
-        public void onSuccess(Widget result) {
-          // Initialize the widget and add it to the page
-          Widget widget = result;
-          if (widget != null) {
-            vPanel.add(widget);
-          }
-          onInitializeComplete();
-        }
-      });
-    }
-  }
-
-  /**
-   * When the widget is first initialize, this method is called. If it returns a
-   * Widget, the widget will be added as the first tab. Return null to disable
+   * When the widget is first initialized, this method is called. If it returns
+   * a Widget, the widget will be added as the first tab. Return null to disable
    * the first tab.
    * 
    * @return the widget to add to the first tab
@@ -315,10 +250,69 @@
 
   protected abstract void asyncOnInitialize(final AsyncCallback<Widget> callback);
 
+  /**
+   * Initialize this widget by creating the elements that should be added to the
+   * page.
+   */
+  protected final Widget createWidget() {
+    deckPanel = new DeckPanel();
+
+    setStyleName(DEFAULT_STYLE_NAME);
+
+    // Add a tab handler
+    tabBar.addSelectionHandler(this);
+
+    // Create a container for the main example
+    final VerticalPanel vPanel = new VerticalPanel();
+    add(vPanel, constants.contentWidgetExample());
+
+    // Add the name
+    HTML nameWidget = new HTML(getName());
+    nameWidget.setStyleName(DEFAULT_STYLE_NAME + "-name");
+    vPanel.add(nameWidget);
+
+    // Add the description
+    HTML descWidget = new HTML(getDescription());
+    descWidget.setStyleName(DEFAULT_STYLE_NAME + "-description");
+    vPanel.add(descWidget);
+
+    // Add source code tab
+    if (hasSource()) {
+      sourceWidget = new HTML();
+      add(sourceWidget, constants.contentWidgetSource());
+    } else {
+      sourceLoaded = true;
+    }
+
+    // Add style tab
+    if (hasStyle()) {
+      styleDefs = new HashMap<String, String>();
+      styleWidget = new HTML();
+      add(styleWidget, constants.contentWidgetStyle());
+    }
+
+    asyncOnInitialize(new AsyncCallback<Widget>() {
+
+      public void onFailure(Throwable caught) {
+        Window.alert("exception: " + caught);
+      }
+
+      public void onSuccess(Widget result) {
+        // Initialize the showcase widget (if any) and add it to the page
+        Widget widget = result;
+        if (widget != null) {
+          vPanel.add(widget);
+        }
+        onInitializeComplete();
+      }
+    });
+
+    return deckPanel;
+  }
+
   @Override
   protected void onLoad() {
-    // Initialize this widget if we haven't already
-    initialize();
+    ensureWidget();
 
     // Select the first tab
     if (getTabBar().getTabCount() > 0) {
diff --git a/user/javadoc/com/google/gwt/examples/LazyPanelExample.java b/user/javadoc/com/google/gwt/examples/LazyPanelExample.java
new file mode 100644
index 0000000..6dead55
--- /dev/null
+++ b/user/javadoc/com/google/gwt/examples/LazyPanelExample.java
@@ -0,0 +1,54 @@
+/*
+ * 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.examples;
+
+import com.google.gwt.core.client.EntryPoint;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.LazyPanel;
+import com.google.gwt.user.client.ui.PushButton;
+import com.google.gwt.user.client.ui.RootPanel;
+import com.google.gwt.user.client.ui.Widget;
+
+public class LazyPanelExample implements EntryPoint {
+
+  /**
+   * A friendly little LazyPanel.
+   */
+  static class HelloLazy extends LazyPanel {
+    @Override
+    protected Widget createWidget() {
+      return new Label("Well hello there!");
+    }
+  }
+
+  public void onModuleLoad() {
+    final Widget lazy = new HelloLazy(); 
+    lazy.setVisible(false);
+
+    PushButton b = new PushButton("Click me");    
+    b.addClickHandler(new ClickHandler() {
+      public void onClick(ClickEvent event) {
+        lazy.setVisible(true);
+      }
+    });
+    
+    RootPanel root = RootPanel.get();
+    root.add(b);
+    root.add(lazy);
+  }
+}
diff --git a/user/src/com/google/gwt/user/client/ui/DisclosurePanel.java b/user/src/com/google/gwt/user/client/ui/DisclosurePanel.java
index 12ee898..32e6be8 100644
--- a/user/src/com/google/gwt/user/client/ui/DisclosurePanel.java
+++ b/user/src/com/google/gwt/user/client/ui/DisclosurePanel.java
@@ -89,6 +89,10 @@
         run(ANIMATION_DURATION);
       } else {
         panel.contentWrapper.setVisible(panel.isOpen);
+        if (panel.isOpen) {
+          // Special treatment on the visible case to ensure LazyPanel works 
+          panel.getContent().setVisible(true);
+        }
       }
     }
 
@@ -107,7 +111,9 @@
       super.onStart();
       if (opening) {
         curPanel.contentWrapper.setVisible(true);
-      }
+        // Special treatment on the visible case to ensure LazyPanel works 
+        curPanel.getContent().setVisible(true);
+     }
     }
 
     @Override
diff --git a/user/src/com/google/gwt/user/client/ui/LazyPanel.java b/user/src/com/google/gwt/user/client/ui/LazyPanel.java
new file mode 100644
index 0000000..5aca1a2
--- /dev/null
+++ b/user/src/com/google/gwt/user/client/ui/LazyPanel.java
@@ -0,0 +1,68 @@
+/*

+ * 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.user.client.ui;

+

+/**

+ * Convenience class to help lazy loading. The bulk of a LazyPanel is not

+ * instantiated until {@link #setVisible}(true) or {@link #ensureWidget} is

+ * called.

+ * <p>

+ * <h3>Example</h3>

+ * {@example com.google.gwt.examples.LazyPanelExample}

+ */

+public abstract class LazyPanel extends SimplePanel {

+  

+  public LazyPanel() {

+  }

+

+  /**

+   * Create the widget contained within the {@link LazyPanel}.

+   * 

+   * @return the lazy widget

+   */

+  protected abstract Widget createWidget();

+

+  /**

+   * Ensures that the widget has been created by calling {@link #createWidget}

+   * if {@link #getWidget} returns <code>null</code>. Typically it is not

+   * necessary to call this directly, as it is called as a side effect of a

+   * <code>setVisible(true)</code> call.

+   */

+  public void ensureWidget() {

+    Widget widget = getWidget();

+    if (widget == null) {

+      widget = createWidget();

+      setWidget(widget);

+    }

+  }

+

+  @Override

+  /**

+   * Sets whether this object is visible. If <code>visible</code> is 

+   * <code>true</code>, creates the sole child widget if necessary by calling

+   * {@link #ensureWidget}.

+   * 

+   * @param visible <code>true</code> to show the object, <code>false</code>

+   * to hide it

+   */

+  public void setVisible(boolean visible) {

+    if (visible) {

+      ensureWidget();

+    }

+    super.setVisible(visible);

+  }

+}

diff --git a/user/src/com/google/gwt/user/client/ui/TabPanel.java b/user/src/com/google/gwt/user/client/ui/TabPanel.java
index e15f772..2203703 100644
--- a/user/src/com/google/gwt/user/client/ui/TabPanel.java
+++ b/user/src/com/google/gwt/user/client/ui/TabPanel.java
@@ -42,9 +42,12 @@
  * {@link com.google.gwt.user.client.ui.HasWidgets}.
  * </p>
  * 
- * <h3>CSS Style Rules</h3> <ul class='css'> <li>.gwt-TabPanel { the tab panel
- * itself }</li> <li>.gwt-TabPanelBottom { the bottom section of the tab panel
- * (the deck containing the widget) }</li> </ul>
+ * <h3>CSS Style Rules</h3> 
+ * <ul class='css'> 
+ * <li>.gwt-TabPanel { the tab panel itself }</li> 
+ * <li>.gwt-TabPanelBottom { the bottom section of the tab panel
+ * (the deck containing the widget) }</li> 
+ * </ul>
  * 
  * <p>
  * <h3>Example</h3>
diff --git a/user/test/com/google/gwt/user/UISuite.java b/user/test/com/google/gwt/user/UISuite.java
index 1452667..1f06f51 100644
--- a/user/test/com/google/gwt/user/UISuite.java
+++ b/user/test/com/google/gwt/user/UISuite.java
@@ -47,6 +47,7 @@
 import com.google.gwt.user.client.ui.HorizontalPanelTest;
 import com.google.gwt.user.client.ui.HyperlinkTest;
 import com.google.gwt.user.client.ui.ImageTest;
+import com.google.gwt.user.client.ui.LazyPanelTest;
 import com.google.gwt.user.client.ui.LinearPanelTest;
 import com.google.gwt.user.client.ui.ListBoxTest;
 import com.google.gwt.user.client.ui.MenuBarTest;
@@ -78,7 +79,7 @@
 import junit.framework.Test;
 
 /**
- * TODO: document me.
+ * Tests of the ui package.
  */
 public class UISuite {
   public static Test suite() {
@@ -120,6 +121,7 @@
     suite.addTestSuite(HyperlinkTest.class);
     suite.addTestSuite(ImageBundleGeneratorTest.class);
     suite.addTestSuite(ImageTest.class);
+    suite.addTestSuite(LazyPanelTest.class);
     suite.addTestSuite(LinearPanelTest.class);
     suite.addTestSuite(ListBoxTest.class);
     suite.addTestSuite(MenuBarTest.class);
diff --git a/user/test/com/google/gwt/user/client/ui/CompositeTest.java b/user/test/com/google/gwt/user/client/ui/CompositeTest.java
index b037981..37abcf8 100644
--- a/user/test/com/google/gwt/user/client/ui/CompositeTest.java
+++ b/user/test/com/google/gwt/user/client/ui/CompositeTest.java
@@ -44,6 +44,7 @@
     boolean domFocusFired;
     boolean domBlurFired;
 
+    @SuppressWarnings("deprecation")
     public EventTestComposite() {
       initWidget(tb);
       sinkEvents(Event.FOCUSEVENTS);
diff --git a/user/test/com/google/gwt/user/client/ui/LazyPanelTest.java b/user/test/com/google/gwt/user/client/ui/LazyPanelTest.java
new file mode 100644
index 0000000..dfd0e3c
--- /dev/null
+++ b/user/test/com/google/gwt/user/client/ui/LazyPanelTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.user.client.ui;
+
+import com.google.gwt.junit.client.GWTTestCase;
+
+/**
+ * Tests {@link LazyPanel}.
+ */
+public class LazyPanelTest extends GWTTestCase {
+  private final static class MyLazyPanel extends LazyPanel {
+    final Widget widgetToCreate;
+    boolean createWasCalled;
+
+    public MyLazyPanel(Widget widgetToCreate) {
+      this.widgetToCreate = widgetToCreate;
+    }
+
+    @Override
+    protected Widget createWidget() {
+      createWasCalled = true;
+      return widgetToCreate;
+    }
+  }
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.user.User";
+  }
+
+  public void testSetVisible() {
+    Widget w = new Label();
+    MyLazyPanel p = new MyLazyPanel(w);
+    assertNull(p.getWidget());
+    assertNull(w.getParent());
+
+    RootPanel.get().add(p);
+    assertNull(w.getParent());
+
+    p.setVisible(true);
+    assertWidgetIsChildOf(w, p);
+    
+    p.createWasCalled = false;
+    p.setVisible(false);
+    assertFalse(p.createWasCalled);
+    
+    p.setVisible(true);
+    assertFalse("Should not call createWidget again", p.createWasCalled);
+  }
+
+  public void testEnsureWidget() {
+    Widget w = new Label();
+    MyLazyPanel p = new MyLazyPanel(w);
+    
+    p.ensureWidget();
+    assertWidgetIsChildOf(w, p);
+    assertEquals(w, p.getWidget());
+  }
+
+  private void assertWidgetIsChildOf(Widget w, Widget p) {
+    Widget parentCursor = w;
+    while (parentCursor != null && parentCursor != RootPanel.get()) {
+      parentCursor = parentCursor.getParent();
+      if (p.equals(parentCursor)) {
+        break;
+      }
+    }
+    assertEquals("Expect w to be child of p", p, parentCursor);
+  }
+  
+  public void testInDeckPanel() {
+    // There are separate paths for the first widget displayed 
+    // and for succeeding, so test both (see DeckPanel#showWidget)
+    
+    DeckPanel deck = new DeckPanel();
+
+    Widget w0 = new Label();
+    deck.insert(new MyLazyPanel(w0), 0);
+    assertNull(w0.getParent());
+
+    Widget w1 = new Label();
+    deck.insert(new MyLazyPanel(w1), 1);
+    assertNull(w0.getParent());
+    assertNull(w1.getParent());
+    
+    deck.showWidget(0);
+    assertWidgetIsChildOf(w0, deck);
+    assertNull(w1.getParent());
+
+    deck.showWidget(1);
+    assertWidgetIsChildOf(w1, deck);
+  }
+  
+  public void testInStackPanel() {
+    StackPanel stack = new StackPanel();
+    stack.add(new Label(), "Able");
+
+    Widget w = new Label();
+    stack.add(new MyLazyPanel(w), "Baker");
+    assertNull(w.getParent());
+    
+    stack.showStack(1);
+    assertWidgetIsChildOf(w, stack);
+  }
+  
+  public void testInDisclosurePanel() {
+    Widget w = new Label();
+    DisclosurePanel dp = new DisclosurePanel();
+
+    dp.add(new MyLazyPanel(w));
+    assertNull(w.getParent());
+    
+    dp.setOpen(true);
+    assertWidgetIsChildOf(w, dp);
+  }
+  
+  public void testInAnimatedDisclosurePanel() {
+    Widget w = new Label();
+    DisclosurePanel dp = new DisclosurePanel();
+    dp.setAnimationEnabled(true);
+
+    dp.add(new MyLazyPanel(w));
+    assertNull(w.getParent());
+    
+    dp.setOpen(true);
+    assertWidgetIsChildOf(w, dp);
+  }
+}