Super Dev Mode: move RecompileListener event handling to Job

No externally visible changes.

Rationale: simplify event handling by putting Job in charge
of reporting events to listeners.

Other refactoring:
 - simplify initWithoutRecompile() so it doesn't need a Job.
   (It's not really a compile job anyway.)
 - simplify progress reporting in Recompiler by making Job
keep track of progress state.

Change-Id: Iec6639d31948a649c79941bacc2429efc6a7d548
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/CodeServer.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/CodeServer.java
index 6da73ba..c23fecd 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/CodeServer.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/CodeServer.java
@@ -75,7 +75,7 @@
         try {
           // TODO: actually test recompiling here.
           // (This is just running precompiles repeatedly.)
-          outboxes.defaultCompileAll(options.getNoPrecompile(), logger);
+          outboxes.defaultCompileAll(logger);
         } catch (Throwable t) {
           t.printStackTrace();
           System.out.println("FAIL");
@@ -152,7 +152,7 @@
       String outboxId = moduleName + "_" + nextOutboxId;
       nextOutboxId++;
 
-      outboxes.addOutbox(new Outbox(outboxId, recompiler, options.getNoPrecompile(), logger));
+      outboxes.addOutbox(new Outbox(outboxId, recompiler, options, logger));
     }
     return outboxes;
   }
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/Job.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/Job.java
index cc913b9..586d3ca 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/Job.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/Job.java
@@ -40,6 +40,8 @@
   private static final ConcurrentMap<String, AtomicInteger> prefixToNextId =
       new ConcurrentHashMap<String, AtomicInteger>();
 
+  // Primary key
+
   private final String id;
 
   // Input
@@ -55,24 +57,45 @@
   // Listeners
 
   private final Outbox outbox;
-
+  private final RecompileListener recompileListener;
   private final LogSupplier logSupplier;
 
   private ProgressTable table; // non-null when submitted
 
+  // Progress
+
+  /**
+   * The number of calls to {@link #onCompilerProgress}.
+   */
+  private int finishedSteps = 0;
+
+  /**
+   * The estimated total number of calls to {@link #onCompilerProgress}.
+   */
+  private int totalSteps = -1; // non-negative after the compile has started
+
+  /**
+   * The id to report to the recompile listener.
+   */
+  private int compileId = -1; // non-negative after the compile has started
+
+  private Exception recompileListenerFailure;
+
   /**
    * Creates a job to update an outbox.
    * @param bindingProperties  Properties that uniquely identify a permutation.
    *     (Otherwise, more than one permutation will be compiled.)
    * @param parentLogger  The parent of the logger that will be used for this job.
    */
-  Job(Outbox box, Map<String, String> bindingProperties, TreeLogger parentLogger) {
+  Job(Outbox box, Map<String, String> bindingProperties,
+      TreeLogger parentLogger, RecompileListener recompileListener) {
     this.id = chooseNextId(box);
     this.outbox = box;
     this.inputModuleName = box.getInputModuleName();
     // TODO: we will use the binding properties to find or create the outbox,
     // then take binding properties from the outbox here.
     this.bindingProperties = ImmutableSortedMap.copyOf(bindingProperties);
+    this.recompileListener = recompileListener;
     this.logSupplier = new LogSupplier(parentLogger, id);
   }
 
@@ -134,6 +157,10 @@
     return result;
   }
 
+  Exception getRecompileListenerFailure() {
+    return recompileListenerFailure;
+  }
+
   // === state transitions ===
 
   /**
@@ -162,14 +189,40 @@
   }
 
   /**
-   * Reports that this job has made progress.
-   * @throws IllegalStateException if the job is not running.
+   * Reports that we started to compile the job.
    */
-  synchronized void onCompilerProgress(Progress.Compiling newProgress) {
+  synchronized void onStarted(int totalSteps, int compileId, CompileDir compileDir) {
+    if (totalSteps < 0) {
+      throw new IllegalArgumentException("totalSteps should not be negative: " + totalSteps);
+    }
     if (table == null || !table.isActive(this)) {
       throw new IllegalStateException("compile job is not active: " + id);
     }
-    table.publish(newProgress, getLogger());
+    if (this.totalSteps >= 0) {
+      throw new IllegalStateException("onStarted already called for " + id);
+    }
+    this.totalSteps = totalSteps;
+    this.compileId = compileId;
+
+    try {
+      recompileListener.startedCompile(inputModuleName, compileId, compileDir);
+    } catch (Exception e) {
+      getLogger().log(TreeLogger.Type.WARN, "recompile listener threw exception", e);
+      recompileListenerFailure = e;
+    }
+  }
+
+  /**
+   * Reports that this job has made progress.
+   * @throws IllegalStateException if the job is not running.
+   */
+  synchronized void onCompilerProgress(String stepMessage) {
+    if (table == null || !table.isActive(this)) {
+      throw new IllegalStateException("compile job is not active: " + id);
+    }
+    finishedSteps++;
+    table.publish(new Progress.Compiling(this, finishedSteps, totalSteps, stepMessage),
+        getLogger());
   }
 
   /**
@@ -180,6 +233,17 @@
     if (table == null || !table.isActive(this)) {
       throw new IllegalStateException("compile job is not active: " + id);
     }
+
+    // Report that we finished unless the listener messed up already.
+    if (recompileListenerFailure == null) {
+      try {
+        recompileListener.finishedCompile(inputModuleName, compileId, newResult.isOk());
+      } catch (Exception e) {
+        getLogger().log(TreeLogger.Type.WARN, "recompile listener threw exception", e);
+        recompileListenerFailure = e;
+      }
+    }
+
     result.set(newResult);
     if (newResult.isOk()) {
       table.publish(new Progress(this, Status.SERVING), getLogger());
@@ -224,8 +288,6 @@
    */
   static class Result {
 
-    final Job job;
-
     /**
      * non-null if successful
      */
@@ -236,10 +298,8 @@
      */
     final Throwable error;
 
-    Result(Job job, CompileDir outputDir, Throwable error) {
-      assert job != null;
+    Result(CompileDir outputDir, Throwable error) {
       assert (outputDir == null) != (error == null);
-      this.job = job;
       this.outputDir = outputDir;
       this.error = error;
     }
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/Outbox.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/Outbox.java
index 305f1a5..f2275f2 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/Outbox.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/Outbox.java
@@ -44,14 +44,17 @@
 
   private final String id;
   private final Recompiler recompiler;
+  private final Options options;
 
-  private final AtomicReference<Job.Result> published = new AtomicReference<Job.Result>();
+  private final AtomicReference<Result> published = new AtomicReference<Result>();
+  private Job publishedJob; // may be null if the Result wasn't created by a Job.
 
-  Outbox(String id, Recompiler recompiler, boolean noPrecompile, TreeLogger logger)
+  Outbox(String id, Recompiler recompiler, Options options, TreeLogger logger)
       throws UnableToCompleteException {
     this.id = id;
     this.recompiler = recompiler;
-    maybePrecompile(noPrecompile, logger);
+    this.options = options;
+    maybePrecompile(logger);
   }
 
   /**
@@ -65,8 +68,14 @@
    * Loads the module and maybe compiles it. Sets up the output directory.
    * Throws an exception if unable. (In this case, Super Dev Mode fails to start.)
    */
-  void maybePrecompile(boolean noPrecompile, TreeLogger logger) throws UnableToCompleteException {
-    // TODO: each box will have its own binding properties.
+  void maybePrecompile(TreeLogger logger) throws UnableToCompleteException {
+
+    if (options.getNoPrecompile()) {
+      publish(recompiler.initWithoutPrecompile(logger), null);
+      return;
+    }
+
+    // TODO: each box will have its own binding properties
     Map<String, String> defaultProps = new HashMap<String, String>();
     defaultProps.put("user.agent", "safari");
     defaultProps.put("locale", "en");
@@ -74,17 +83,29 @@
     // Create a dummy job for the first compile.
     // Its progress is not visible externally but will still be logged.
     ProgressTable dummy = new ProgressTable();
-    Job job = new Job(this, defaultProps, logger);
+    Job job = makeJob(defaultProps, logger);
     job.onSubmitted(dummy);
+    publish(recompiler.precompile(job), job);
 
-    if (noPrecompile) {
-      publish(recompiler.initWithoutPrecompile(job));
-    } else {
-      publish(recompiler.precompile(job));
+    if (options.isCompileTest()) {
+      // Listener errors are fatal in compile tests
+      Throwable error = job.getRecompileListenerFailure();
+      if (error != null) {
+        UnableToCompleteException e = new UnableToCompleteException();
+        e.initCause(error);
+        throw e;
+      }
     }
   }
 
   /**
+   * Creates a Job whose output will be saved in this outbox.
+   */
+  Job makeJob(Map<String, String> bindingProperties, TreeLogger parentLogger) {
+    return new Job(this, bindingProperties, parentLogger, options.getRecompileListener());
+  }
+
+  /**
    * Compiles the module again, possibly changing the output directory.
    * After returning, the result of the compile can be found via {@link Job#waitForResult}
    */
@@ -97,7 +118,7 @@
     Result result = recompiler.recompile(job);
 
     if (result.isOk()) {
-      publish(result);
+      publish(result, job);
     } else {
       job.getLogger().log(TreeLogger.Type.WARN, "continuing to serve previous version");
     }
@@ -105,12 +126,14 @@
 
   /**
    * Makes the result of a compile downloadable via HTTP.
+   * @param job the job that created this result, or null if none.
    */
-  private void publish(Result result) {
-    Result previous = published.getAndSet(result);
-    if (previous != null) {
-      previous.job.onGone();
+  private synchronized void publish(Result result, Job job) {
+    if (publishedJob != null) {
+      publishedJob.onGone();
     }
+    publishedJob = job;
+    published.set(result);
   }
 
   private CompileDir getOutputDir() {
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/OutboxTable.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/OutboxTable.java
index 400a3eb..54ce117 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/OutboxTable.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/OutboxTable.java
@@ -69,9 +69,9 @@
     return result;
   }
 
-  void defaultCompileAll(boolean noPrecompile, TreeLogger logger) throws UnableToCompleteException {
+  void defaultCompileAll(TreeLogger logger) throws UnableToCompleteException {
     for (Outbox box: outboxes.values()) {
-      box.maybePrecompile(noPrecompile, logger);
+      box.maybePrecompile(logger);
     }
   }
 }
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java
index 247b4ee..9a27ed6 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java
@@ -116,10 +116,10 @@
     } catch (UnableToCompleteException e) {
       // No point in logging a stack trace for this exception
       job.getLogger().log(TreeLogger.Type.WARN, "recompile failed");
-      result = new Result(job, null, e);
+      result = new Result(null, e);
     } catch (Throwable error) {
       job.getLogger().log(TreeLogger.Type.WARN, "recompile failed", error);
-      result = new Result(job, null, error);
+      result = new Result(null, error);
     }
 
     job.onFinished(result);
@@ -134,8 +134,7 @@
    * @return a non-error Job.Result if successful.
    * @throws UnableToCompleteException for compile failures.
    */
-  private Job.Result compile(Job job)
-      throws UnableToCompleteException {
+  private Job.Result compile(Job job) throws UnableToCompleteException {
 
     assert job.wasSubmitted();
 
@@ -154,31 +153,15 @@
     CompileDir compileDir = makeCompileDir(compileId, job.getLogger());
     TreeLogger compileLogger = makeCompileLogger(compileDir, job.getLogger());
 
-    boolean listenerFailed = false;
-    try {
-      options.getRecompileListener().startedCompile(inputModuleName, compileId, compileDir);
-    } catch (Exception e) {
-      compileLogger.log(TreeLogger.Type.WARN, "listener threw exception", e);
-      listenerFailed = true;
-    }
+    int totalSteps = options.shouldCompileIncremental() ? 1 : 2;
+    job.onStarted(totalSteps, compileId, compileDir);
 
-    boolean success = false;
-    try {
-      if (options.shouldCompileIncremental()) {
-        // Just have one message for now.
-        job.onCompilerProgress(new Progress.Compiling(job, 0, 1, "Compiling"));
-
-        success = compileIncremental(compileLogger, compileDir);
-      } else {
-        success = compileMonolithic(compileLogger, compileDir, job);
-      }
-    } finally {
-      try {
-        options.getRecompileListener().finishedCompile(inputModuleName, compilesDone, success);
-      } catch (Exception e) {
-        compileLogger.log(TreeLogger.Type.WARN, "listener threw exception", e);
-        listenerFailed = true;
-      }
+    boolean success;
+    if (options.shouldCompileIncremental()) {
+      job.onCompilerProgress("Compiling (incrementally)");
+      success = compileIncremental(compileLogger, compileDir);
+    } else {
+      success = compileMonolithic(compileLogger, compileDir, job);
     }
 
     if (!success) {
@@ -190,22 +173,19 @@
     compileLogger.log(TreeLogger.Type.INFO,
         String.format("%.3fs total -- Compile completed", elapsedTime / 1000d));
 
-    if (options.isCompileTest() && listenerFailed) {
-      throw new UnableToCompleteException();
-    }
-
-    return new Result(job, publishedCompileDir, null);
+    return new Result(publishedCompileDir, null);
   }
 
   /**
    * Creates a dummy output directory without compiling the module.
    * Either this method or {@link #precompile} should be called first.
    */
-  synchronized Job.Result initWithoutPrecompile(Job job) throws UnableToCompleteException {
+  synchronized Job.Result initWithoutPrecompile(TreeLogger logger)
+      throws UnableToCompleteException {
 
     long startTime = System.currentTimeMillis();
-    CompileDir compileDir = makeCompileDir(++compilesDone, job.getLogger());
-    TreeLogger compileLogger = makeCompileLogger(compileDir, job.getLogger());
+    CompileDir compileDir = makeCompileDir(++compilesDone, logger);
+    TreeLogger compileLogger = makeCompileLogger(compileDir, logger);
 
     ModuleDef module = loadModule(compileLogger);
     String newModuleName = module.getName();  // includes any rename.
@@ -235,9 +215,7 @@
     long elapsedTime = System.currentTimeMillis() - startTime;
     compileLogger.log(TreeLogger.Type.INFO, "Module setup completed in " + elapsedTime + " ms");
 
-    Result result = new Result(job, compileDir, null);
-    job.onFinished(result);
-    return result;
+    return new Result(compileDir, null);
   }
 
   private boolean compileIncremental(TreeLogger compileLogger, CompileDir compileDir) {
@@ -276,8 +254,7 @@
   private boolean compileMonolithic(TreeLogger compileLogger, CompileDir compileDir, Job job)
       throws UnableToCompleteException {
 
-    job.onCompilerProgress(
-        new Progress.Compiling(job, 0, 2, "Loading modules"));
+    job.onCompilerProgress("Loading modules");
 
     CompilerOptions loadOptions = new CompilerOptionsImpl(compileDir, inputModuleName, options);
     compilerContext = compilerContextBuilder.options(loadOptions).build();
@@ -297,7 +274,7 @@
       return true;
     }
 
-    job.onCompilerProgress(new Progress.Compiling(job, 1, 2, "Compiling"));
+    job.onCompilerProgress("Compiling");
     // TODO: use speed tracer to get more compiler events?
 
     CompilerOptions runOptions = new CompilerOptionsImpl(compileDir, newModuleName, options);
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java
index 95dcdaf..bcede76 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java
@@ -213,7 +213,7 @@
       // cause a spurious recompile, resulting in an unexpected permutation being loaded later.
       //
       // It would be unsafe to allow a configuration property to be changed.
-      Job job = new Job(outbox, getBindingProperties(request), logger);
+      Job job = outbox.makeJob(getBindingProperties(request), logger);
       runner.submit(job);
       Job.Result result = job.waitForResult();
       JsonObject json = jsonExporter.exportRecompileResponse(result);