Speed up SOYC compilation.

Review by: spoon@google.com



git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@5482 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/core/ext/soyc/Range.java b/dev/core/src/com/google/gwt/core/ext/soyc/Range.java
index 8414263..f98fe68 100644
--- a/dev/core/src/com/google/gwt/core/ext/soyc/Range.java
+++ b/dev/core/src/com/google/gwt/core/ext/soyc/Range.java
@@ -54,8 +54,8 @@
     }
   };
 
-  private final int end;
-  private final int start;
+  final int end;
+  final int start;
 
   /**
    * Constructor.
diff --git a/dev/core/src/com/google/gwt/core/ext/soyc/Story.java b/dev/core/src/com/google/gwt/core/ext/soyc/Story.java
index e0f3ad4..f1eac77 100644
--- a/dev/core/src/com/google/gwt/core/ext/soyc/Story.java
+++ b/dev/core/src/com/google/gwt/core/ext/soyc/Story.java
@@ -31,6 +31,7 @@
 package com.google.gwt.core.ext.soyc;
 
 import java.io.Serializable;
+import java.util.Set;
 import java.util.SortedSet;
 
 /**
@@ -82,6 +83,5 @@
    * string literals) that appear in multiple locations in the source may be
    * merged by the compiler into a single Story.
    */
-  SortedSet<Origin> getSourceOrigin();
-
-}
\ No newline at end of file
+  Set<Origin> getSourceOrigin();
+}
diff --git a/dev/core/src/com/google/gwt/core/ext/soyc/impl/DependencyRecorder.java b/dev/core/src/com/google/gwt/core/ext/soyc/impl/DependencyRecorder.java
index f5335ca..9bdc649 100644
--- a/dev/core/src/com/google/gwt/core/ext/soyc/impl/DependencyRecorder.java
+++ b/dev/core/src/com/google/gwt/core/ext/soyc/impl/DependencyRecorder.java
@@ -19,12 +19,10 @@
 import com.google.gwt.dev.jjs.ast.JMethod;
 import com.google.gwt.dev.jjs.ast.JProgram;
 import com.google.gwt.dev.jjs.impl.ControlFlowAnalyzer;
-import com.google.gwt.dev.util.HtmlTextOutput;
 import com.google.gwt.util.tools.Utility;
 
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
-import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.zip.GZIPOutputStream;
 
@@ -42,10 +40,7 @@
     new DependencyRecorder().recordDependenciesImpl(logger, out, jprogram);
   }
 
-  private HtmlTextOutput htmlOut;
-  private PrintWriter pw;
-
-  private OutputStreamWriter writer;
+  private StringBuilder builder;
 
   private DependencyRecorder() {
   }
@@ -74,22 +69,29 @@
     dependencyAnalyzer.setDependencyRecorder(this);
 
     try {
-      writer = new OutputStreamWriter(new GZIPOutputStream(out), "UTF-8");
-      pw = new PrintWriter(writer);
-      htmlOut = new HtmlTextOutput(pw, false);
+      OutputStreamWriter writer
+         = new OutputStreamWriter(new GZIPOutputStream(out), "UTF-8");
+      
+      StringBuilder localBuilder = new StringBuilder();
+      this.builder = localBuilder;
+
+      printPre();
+      for (JMethod method : jprogram.getAllEntryMethods()) {
+        dependencyAnalyzer.traverseFrom(method);
+        if (localBuilder.length() > 8 * 1024) {
+          writer.write(localBuilder.toString());
+          localBuilder.setLength(0);
+        }
+      }
+      printPost();
+
+      writer.write(localBuilder.toString());
+      Utility.close(writer);
+
+      logger.log(TreeLogger.INFO, "Done");
     } catch (Throwable e) {
-      logger.log(TreeLogger.ERROR, "Could not open dependency file.", e);
+      logger.log(TreeLogger.ERROR, "Could not write dependency file.", e);
     }
-
-    printPre();
-    for (JMethod method : jprogram.getAllEntryMethods()) {
-      dependencyAnalyzer.traverseFrom(method);
-    }
-    printPost();
-    pw.close();
-    Utility.close(writer);
-
-    logger.log(TreeLogger.INFO, "Done");
   }
 
   /**
@@ -99,55 +101,45 @@
    * @param dependencyChain
    */
   private void printMethodDependency(ArrayList<JMethod> dependencyChain) {
-    String curLine;
-    for (int i = dependencyChain.size() - 1; i >= 0; i--) {
-      JMethod curMethod = dependencyChain.get(i);
-      String sFullMethodString = curMethod.getName();
-      if (curMethod.getEnclosingType() != null) {
-        sFullMethodString = curMethod.getEnclosingType().getName() + "::"
-            + curMethod.getName();
-      }
-      if (i == dependencyChain.size() - 1) {
-        curLine = "<method name=\"" + sFullMethodString + "\">";
-        htmlOut.printRaw(curLine);
-        htmlOut.newline();
-        htmlOut.indentIn();
-        htmlOut.indentIn();
-      } else {
-        curLine = "<called by=\"" + sFullMethodString + "\"/>";
-        htmlOut.printRaw(curLine);
-        htmlOut.newline();
-      }
+    int size = dependencyChain.size();
+    if (size == 0) {
+      return;
     }
-    htmlOut.indentOut();
-    htmlOut.indentOut();
-    curLine = "</method>";
-    htmlOut.printRaw(curLine);
-    htmlOut.newline();
+    
+    JMethod curMethod = dependencyChain.get(size - 1);
+    builder.append("<method name=\"");
+    if (curMethod.getEnclosingType() != null) {
+      builder.append(curMethod.getEnclosingType().getName());
+      builder.append("::");
+    }
+    builder.append(curMethod.getName());
+    builder.append("\">\n");
+
+    for (int i = size - 2; i >= 0; i--) {
+      curMethod = dependencyChain.get(i);
+      builder.append("<called by=\"");
+      if (curMethod.getEnclosingType() != null) {
+        builder.append(curMethod.getEnclosingType().getName());
+        builder.append("::");
+      }
+      builder.append(curMethod.getName());
+      builder.append("\"/>\n");
+    }
+    builder.append("</method>\n");
   }
 
   /**
    * Prints the closing lines necessary for the dependencies file.
    */
   private void printPost() {
-    String curLine = "</soyc-dependencies>";
-    htmlOut.indentOut();
-    htmlOut.indentOut();
-    htmlOut.printRaw(curLine);
-    htmlOut.newline();
+    builder.append("</soyc-dependencies>\n");
   }
 
   /**
    * Prints the preamble necessary for the dependencies file.
    */
   private void printPre() {
-    String curLine = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
-    htmlOut.printRaw(curLine);
-    htmlOut.newline();
-    curLine = "<soyc-dependencies>";
-    htmlOut.printRaw(curLine);
-    htmlOut.newline();
-    htmlOut.indentIn();
-    htmlOut.indentIn();
+    builder.append(
+        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<soyc-dependencies>\n");
   }
 }
diff --git a/dev/core/src/com/google/gwt/core/ext/soyc/impl/StoryImpl.java b/dev/core/src/com/google/gwt/core/ext/soyc/impl/StoryImpl.java
index 6526b3e..b99bcbb 100644
--- a/dev/core/src/com/google/gwt/core/ext/soyc/impl/StoryImpl.java
+++ b/dev/core/src/com/google/gwt/core/ext/soyc/impl/StoryImpl.java
@@ -19,8 +19,8 @@
 import com.google.gwt.core.ext.soyc.Story;
 
 import java.io.Serializable;
-import java.util.Collections;
 import java.util.Comparator;
+import java.util.Set;
 import java.util.SortedSet;
 
 /**
@@ -43,14 +43,14 @@
   private final int length;
   private final String literalDescription;
   private final SortedSet<Member> members;
-  private final SortedSet<Origin> origins;
+  private final Set<Origin> origins;
 
   /**
    * Standard constructor. This constructor will create unmodifiable versions of
    * the collections passed into it.
    */
   public StoryImpl(int id, SortedSet<Member> members, 
-      SortedSet<Origin> origins, String literalDescription, int fragment,
+      Set<Origin> origins, String literalDescription, int fragment,
       int length) {
     assert members != null;
     assert origins != null;
@@ -63,8 +63,8 @@
     this.length = length;
     this.literalDescription = literalDescription == null ? null
         : literalDescription.intern();
-    this.members = Collections.unmodifiableSortedSet(members);
-    this.origins = Collections.unmodifiableSortedSet(origins);
+    this.members = members;
+    this.origins = origins;
   }
 
   /**
@@ -116,7 +116,7 @@
     return members;
   }
 
-  public SortedSet<Origin> getSourceOrigin() {
+  public Set<Origin> getSourceOrigin() {
     return origins;
   }
 
diff --git a/dev/core/src/com/google/gwt/core/ext/soyc/impl/StoryRecorder.java b/dev/core/src/com/google/gwt/core/ext/soyc/impl/StoryRecorder.java
index c2dfafa..32dfb5d 100644
--- a/dev/core/src/com/google/gwt/core/ext/soyc/impl/StoryRecorder.java
+++ b/dev/core/src/com/google/gwt/core/ext/soyc/impl/StoryRecorder.java
@@ -29,12 +29,11 @@
 import com.google.gwt.dev.jjs.ast.JField;
 import com.google.gwt.dev.jjs.ast.JMethod;
 import com.google.gwt.dev.js.ast.JsFunction;
-import com.google.gwt.dev.util.HtmlTextOutput;
+import com.google.gwt.dev.util.Util;
 import com.google.gwt.util.tools.Utility;
 
+import java.io.IOException;
 import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.PrintWriter;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -67,6 +66,8 @@
     }
   }
 
+  private static final int MAX_STRING_BUILDER_SIZE = 65536;
+
   /**
    * Used to record dependencies of a program.
    */
@@ -75,35 +76,35 @@
     new StoryRecorder().recordStoriesImpl(logger, out, sourceInfoMaps, js);
   }
 
+  private StringBuilder builder;
+
   private int curHighestFragment = 0;
 
-  private HtmlTextOutput htmlOut;
+  private OutputStream gzipStream;
+  
   private String[] js;
 
   /**
    * Used by {@link #popAndRecord(Stack)} to determine start and end ranges.
    */
   private int lastEnd = 0;
+
   /**
    * This is a class field for convenience, but it should be deleted at the end
    * of the constructor.
    */
   private transient Map<Correlation, Member> membersByCorrelation = new IdentityHashMap<Correlation, Member>();
-  private PrintWriter pw;
-
+  
   /**
    * This is a class field for convenience, but it should be deleted at the end
    * of the constructor.
    */
   private transient Map<SourceInfo, StoryImpl> storyCache = new IdentityHashMap<SourceInfo, StoryImpl>();
-
   private Map<Story, Integer> storyIds = new HashMap<Story, Integer>();
 
-  private OutputStreamWriter writer;
-
   private StoryRecorder() {
   }
-
+  
   protected void recordStoriesImpl(TreeLogger logger, OutputStream out,
       List<Map<Range, SourceInfo>> sourceInfoMaps, String[] js) {
 
@@ -112,25 +113,8 @@
     this.js = js;
 
     try {
-      writer = new OutputStreamWriter(new GZIPOutputStream(out), "UTF-8");
-      pw = new PrintWriter(writer);
-      htmlOut = new HtmlTextOutput(pw, false);
-
-      String curLine = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
-      htmlOut.printRaw(curLine);
-      htmlOut.newline();
-
-      curLine = "<soyc>";
-      htmlOut.printRaw(curLine);
-      htmlOut.newline();
-      htmlOut.indentIn();
-      htmlOut.indentIn();
-
-      curLine = "<stories>";
-      htmlOut.printRaw(curLine);
-      htmlOut.newline();
-      htmlOut.indentIn();
-      htmlOut.indentIn();
+      builder = new StringBuilder(MAX_STRING_BUILDER_SIZE * 2);
+      gzipStream = new GZIPOutputStream(out);
 
       /*
        * Don't retain beyond the constructor to avoid lingering references to
@@ -145,13 +129,20 @@
           Member.SOURCE_NAME_COMPARATOR);
       Set<SourceInfo> sourceInfoSeen = new HashSet<SourceInfo>();
 
+      builder.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<soyc>\n<stories>\n");
+
       int fragment = 0;
       for (Map<Range, SourceInfo> sourceInfoMap : sourceInfoMaps) {
         lastEnd = 0;
         analyzeFragment(memberFactory, classesMutable, functionsMutable,
             sourceInfoMap, sourceInfoSeen, fragment++);
+        
+        // Flush output to improve memory locality
+        flushOutput();
       }
 
+      builder.append("</stories>\n</soyc>\n");
+
       /*
        * Clear the member fields that we don't need anymore to allow GC of the
        * SourceInfo objects
@@ -159,33 +150,20 @@
       membersByCorrelation = null;
       storyCache = null;
 
-      htmlOut.indentOut();
-      htmlOut.indentOut();
-      curLine = "</stories>";
-      htmlOut.printRaw(curLine);
-      htmlOut.newline();
-
-      htmlOut.indentOut();
-      htmlOut.indentOut();
-      curLine = "</soyc>";
-      htmlOut.printRaw(curLine);
-      htmlOut.newline();
-
-      Utility.close(writer);
-      pw.close();
+      Util.writeUtf8(builder, gzipStream);
+      Utility.close(gzipStream);
 
       logger.log(TreeLogger.INFO, "Done");
-
     } catch (Throwable e) {
-      logger.log(TreeLogger.ERROR, "Could not open dependency file.", e);
+      logger.log(TreeLogger.ERROR, "Could not write dependency file.", e);
     }
   }
-
+  
   private void analyzeFragment(MemberFactory memberFactory,
       TreeSet<ClassMember> classesMutable,
       TreeSet<FunctionMember> functionsMutable,
       Map<Range, SourceInfo> sourceInfoMap, Set<SourceInfo> sourceInfoSeen,
-      int fragment) {
+      int fragment) throws IOException {
     /*
      * We want to iterate over the Ranges so that enclosing Ranges come before
      * their enclosed Ranges...
@@ -215,7 +193,10 @@
       // Possibly create and record Members
       if (!sourceInfoSeen.contains(info)) {
         sourceInfoSeen.add(info);
-        for (Correlation c : info.getPrimaryCorrelations()) {
+        for (Correlation c : info.getPrimaryCorrelationsArray()) {
+          if (c == null) {
+            continue;
+          }
           if (membersByCorrelation.containsKey(c)) {
             continue;
           }
@@ -255,17 +236,6 @@
         }
       }
 
-      /*
-       * Record dependencies as observed in the structure of the JS output. This
-       * an an ad-hoc approach that just looks at which SourceInfos are used
-       * within the Range of another SourceInfo.
-       */
-      Set<Correlation> correlationsInScope = new HashSet<Correlation>();
-      for (RangeInfo outer : dependencyScope) {
-        SourceInfo outerInfo = outer.info;
-        correlationsInScope.addAll(outerInfo.getPrimaryCorrelations());
-      }
-
       dependencyScope.push(new RangeInfo(range, info));
     }
 
@@ -283,8 +253,7 @@
     assert dependencyOrder[0].getEnd() == lastEnd;
   }
 
-  private void emitStory(StoryImpl story, Range range) {
-
+  private void emitStory(StoryImpl story, Range range) throws IOException {
     int storyNum;
     if (storyIds.containsKey(story)) {
       storyNum = storyIds.get(story);
@@ -293,109 +262,65 @@
       storyIds.put(story, storyNum);
     }
 
-    String curLine = "<story id=\"story" + Integer.toString(storyNum) + "\"";
+    builder.append("<story id=\"story");
+    builder.append(storyNum);
     if (story.getLiteralTypeName() != null) {
-      curLine = curLine + " literal=\"" + story.getLiteralTypeName() + "\"";
+      builder.append("\" literal=\"");
+      builder.append(story.getLiteralTypeName());
     }
-    curLine = curLine + ">";
-    htmlOut.printRaw(curLine);
-    htmlOut.newline();
+    builder.append("\">\n");
 
     Set<Origin> origins = story.getSourceOrigin();
     if (origins.size() > 0) {
-      htmlOut.indentIn();
-      htmlOut.indentIn();
-
-      curLine = "<origins>";
-      htmlOut.printRaw(curLine);
-      htmlOut.newline();
-
-      htmlOut.indentIn();
-      htmlOut.indentIn();
-    }
-    for (Origin origin : origins) {
-      curLine = "<origin lineNumber=\""
-          + Integer.toString(origin.getLineNumber()) + "\" location=\""
-          + origin.getLocation() + "\"/>";
-      htmlOut.printRaw(curLine);
-      htmlOut.newline();
-    }
-    if (origins.size() > 0) {
-      htmlOut.indentOut();
-      htmlOut.indentOut();
-
-      curLine = "</origins>";
-      htmlOut.printRaw(curLine);
-      htmlOut.newline();
-
-      htmlOut.indentOut();
-      htmlOut.indentOut();
+      builder.append("<origins>\n");
+      for (Origin origin : origins) {
+        builder.append("<origin lineNumber=\"");
+        builder.append(origin.getLineNumber());
+        builder.append("\" location=\"");
+        builder.append(origin.getLocation());
+        builder.append("\"/>\n");
+        
+        flushOutput();
+      }
+      builder.append("</origins>\n");
     }
 
     Set<Member> correlations = story.getMembers();
     if (correlations.size() > 0) {
-      htmlOut.indentIn();
-      htmlOut.indentIn();
-
-      curLine = "<correlations>";
-      htmlOut.printRaw(curLine);
-      htmlOut.newline();
-
-      htmlOut.indentIn();
-      htmlOut.indentIn();
-    }
-    for (Member correlation : correlations) {
-      curLine = "<by idref=\"" + correlation.getSourceName() + "\"/>";
-      htmlOut.printRaw(curLine);
-      htmlOut.newline();
-    }
-    if (correlations.size() > 0) {
-      htmlOut.indentOut();
-      htmlOut.indentOut();
-
-      curLine = "</correlations>";
-      htmlOut.printRaw(curLine);
-      htmlOut.newline();
-
-      htmlOut.indentOut();
-      htmlOut.indentOut();
+      builder.append("<correlations>\n");
+      for (Member correlation : correlations) {
+        builder.append("<by idref=\"");
+        builder.append(correlation.getSourceName());
+        builder.append("\"/>\n");
+        
+        flushOutput();
+      }
+      builder.append("</correlations>\n");
     }
 
-    htmlOut.indentIn();
-    htmlOut.indentIn();
+    builder.append("<js fragment=\"");
+    builder.append(curHighestFragment);
+    builder.append("\"/>\n<storyref idref=\"story");
+    builder.append(storyNum);
 
-    curLine = "<js fragment=\"" + curHighestFragment + "\"/>";
-    htmlOut.printRaw(curLine);
-    htmlOut.newline();
-
-    String jsCode = js[curHighestFragment].substring(range.getStart(),
-        range.getEnd());
-    jsCode = escapeXml(jsCode);
-    if ((jsCode.length() == 0) || (jsCode.compareTo("\n") == 0)) {
-      curLine = "<storyref idref=\"story" + Integer.toString(storyNum) + "\"/>";
+    int start = range.getStart();
+    int end = range.getEnd();
+    String jsCode = js[curHighestFragment];
+    if ((start == end) || ((end == start + 1) && jsCode.charAt(start) == '\n')) {
+      builder.append("\"/>\n</story>\n");
     } else {
-      curLine = "<storyref idref=\"story" + Integer.toString(storyNum) + "\">"
-          + jsCode + "</storyref>";
+      builder.append("\">");
+      Util.escapeXml(jsCode, start, end, false, builder);
+      builder.append("</storyref>\n</story>\n");
     }
-
-    htmlOut.printRaw(curLine);
-    htmlOut.newline();
-
-    htmlOut.indentOut();
-    htmlOut.indentOut();
-
-    curLine = "</story>";
-    htmlOut.printRaw(curLine);
-    htmlOut.newline();
   }
 
-  private String escapeXml(String unescaped) {
-    String escaped = unescaped.replaceAll("\\&", "&amp;");
-    escaped = escaped.replaceAll("\\<", "&lt;");
-    escaped = escaped.replaceAll("\\>", "&gt;");
-    escaped = escaped.replaceAll("\\\"", "&quot;");
-    // escaped = escaped.replaceAll("\\'", "&apos;");
-    return escaped;
+  private void flushOutput() throws IOException {
+    // Flush output to improve memory locality
+    if (builder.length() > MAX_STRING_BUILDER_SIZE) {
+      Util.writeUtf8(builder, gzipStream);
+      builder.setLength(0);
+    }
   }
 
   /**
@@ -403,7 +328,8 @@
    * the right length, possibly sub-dividing the super-enclosing Range in the
    * process.
    */
-  private void popAndRecord(Stack<RangeInfo> dependencyScope, int fragment) {
+  private void popAndRecord(Stack<RangeInfo> dependencyScope, int fragment)
+      throws IOException {
     RangeInfo rangeInfo = dependencyScope.pop();
     Range toStore = rangeInfo.range;
 
@@ -416,7 +342,7 @@
       assert !dependencyScope.isEmpty();
 
       SourceInfo gapInfo = dependencyScope.peek().info;
-      recordStory(gapInfo, fragment, newRange.length(), newRange);
+      recordStory(gapInfo, fragment, newRange.length(), newRange, builder);
 
       lastEnd += newRange.length();
     }
@@ -429,13 +355,13 @@
     if (lastEnd < toStore.getEnd()) {
       Range newRange = new Range(Math.max(lastEnd, toStore.getStart()),
           toStore.getEnd());
-      recordStory(rangeInfo.info, fragment, newRange.length(), newRange);
+      recordStory(rangeInfo.info, fragment, newRange.length(), newRange, builder);
       lastEnd += newRange.length();
     }
   }
 
   private void recordStory(SourceInfo info, int fragment, int length,
-      Range range) {
+      Range range, StringBuilder builder) throws IOException {
     assert info != null;
     assert storyCache != null;
 
@@ -445,20 +371,19 @@
 
     StoryImpl theStory;
     if (!storyCache.containsKey(info)) {
-
       SortedSet<Member> members = new TreeSet<Member>(
           Member.TYPE_AND_SOURCE_NAME_COMPARATOR);
+      Set<Origin> origins = new HashSet<Origin>();
 
       for (Correlation c : info.getAllCorrelations()) {
         Member m = membersByCorrelation.get(c);
         if (m != null) {
           members.add(m);
         }
-      }
-
-      SortedSet<Origin> origins = new TreeSet<Origin>();
-      for (Correlation c : info.getAllCorrelations(Axis.ORIGIN)) {
-        origins.add(new OriginImpl(c.getOrigin()));
+        
+        if (c.getAxis() == Axis.ORIGIN) {
+          origins.add(new OriginImpl(c.getOrigin()));
+        }
       }
 
       String literalType = null;
@@ -477,4 +402,5 @@
 
     emitStory(theStory, range);
   }
+
 }
diff --git a/dev/core/src/com/google/gwt/dev/CompilePerms.java b/dev/core/src/com/google/gwt/dev/CompilePerms.java
index e3bd98d..13929d3 100644
--- a/dev/core/src/com/google/gwt/dev/CompilePerms.java
+++ b/dev/core/src/com/google/gwt/dev/CompilePerms.java
@@ -196,7 +196,7 @@
       List<FileBackedObject<PermutationResult>> resultFiles)
       throws UnableToCompleteException {
     final TreeLogger branch = logger.branch(TreeLogger.INFO, "Compiling "
-        + perms.length + " permutations");
+        + perms.length + " permutation" + (perms.length > 1 ? "s" : ""));
     PermutationWorkerFactory.compilePermutations(logger, precompilation, perms,
         localWorkers, resultFiles);
     branch.log(TreeLogger.INFO, "Permutation compile succeeded");
diff --git a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
index 7b0de33..f440994 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
@@ -183,6 +183,7 @@
   public static PermutationResult compilePermutation(TreeLogger logger,
       UnifiedAst unifiedAst, Map<String, String> rebindAnswers,
       PropertyOracle[] propertyOracles, int permutationId) throws UnableToCompleteException {
+    long permStart = System.currentTimeMillis();
     try {
       if (JProgram.isTracingEnabled()) {
         System.out.println("------------------------------------------------------------");
@@ -323,10 +324,13 @@
       PermutationResult toReturn = new PermutationResultImpl(js,
           makeSymbolMap(symbolTable));
 
-      if (sourceInfoMaps != null) {
+      if (sourceInfoMaps != null) {        
+        long soycStart = System.currentTimeMillis();
+        System.out.println("Computing SOYC output");
         // Free up memory.
         symbolTable = null;
 
+        long start = System.currentTimeMillis();
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
         // get method dependencies
         StoryRecorder.recordStories(logger, baos, sourceInfoMaps, js);
@@ -334,20 +338,34 @@
             + ".xml.gz", baos.toByteArray());
         // Free up memory.
         js = null;
+        System.out.println("Record stories: "
+            + (System.currentTimeMillis() - start) + " ms");
 
+        start = System.currentTimeMillis();
         baos.reset();
         DependencyRecorder.recordDependencies(logger, baos, jprogram);
         SoycArtifact dependencies = new SoycArtifact("dependencies"
             + permutationId + ".xml.gz", baos.toByteArray());
+        System.out.println("Record dependencies: "
+            + (System.currentTimeMillis() - start) + " ms");
 
+        start = System.currentTimeMillis();
         baos.reset();
         SplitPointRecorder.recordSplitPoints(jprogram, baos, logger);
         SoycArtifact splitPoints = new SoycArtifact("splitPoints"
             + permutationId + ".xml.gz", baos.toByteArray());
+        System.out.println("Record split points: "
+            + (System.currentTimeMillis() - start) + " ms");
 
         toReturn.getArtifacts().add(
             new StandardCompilationAnalysis(dependencies, stories, splitPoints));
+        
+        System.out.println("Completed SOYC phase in "
+            + (System.currentTimeMillis() - soycStart) + " ms");
       }
+      
+      System.out.println("Permutation took "
+          + (System.currentTimeMillis() - permStart) + " ms");
       return toReturn;
     } catch (Throwable e) {
       throw logAndTranslateException(logger, e);
diff --git a/dev/core/src/com/google/gwt/dev/jjs/SourceInfo.java b/dev/core/src/com/google/gwt/dev/jjs/SourceInfo.java
index baaf810..a34a2b0 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/SourceInfo.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/SourceInfo.java
@@ -64,6 +64,13 @@
    * has been set.
    */
   Set<Correlation> getPrimaryCorrelations();
+  
+  /**
+   * Returns the first Correlations added along each Axis on which a Correlation
+   * has been set.  Some entries may be null and should be ignored.  The
+   * returned array must not be modified.
+   */
+  Correlation[] getPrimaryCorrelationsArray();
 
   int getStartLine();
 
diff --git a/dev/core/src/com/google/gwt/dev/jjs/SourceInfoCorrelation.java b/dev/core/src/com/google/gwt/dev/jjs/SourceInfoCorrelation.java
index d7221ca..cb96475 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/SourceInfoCorrelation.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/SourceInfoCorrelation.java
@@ -36,9 +36,11 @@
    * Micro-opt for {@link #makeChild(Class, String)}.
    */
   private static final SourceInfo[] EMPTY_SOURCEINFO_ARRAY = new SourceInfo[0];
+  
+  private static final int numCorrelationAxes = Axis.values().length;
 
   private static int numCorrelationAxes() {
-    return Axis.values().length;
+    return numCorrelationAxes;
   }
 
   /**
@@ -165,6 +167,10 @@
     }
     return toReturn;
   }
+  
+  public Correlation[] getPrimaryCorrelationsArray() {
+    return primaryCorrelations;
+  }
 
   public int getStartLine() {
     return getOrigin().getStartLine();
diff --git a/dev/core/src/com/google/gwt/dev/jjs/SourceOrigin.java b/dev/core/src/com/google/gwt/dev/jjs/SourceOrigin.java
index 8224780..f06dba8 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/SourceOrigin.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/SourceOrigin.java
@@ -166,6 +166,10 @@
   public Set<Correlation> getPrimaryCorrelations() {
     return Collections.emptySet();
   }
+  
+  public Correlation[] getPrimaryCorrelationsArray() {
+    return new Correlation[0];
+  }
 
   public int getStartLine() {
     return startLine;
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 70cf7e3..f06b447 100644
--- a/dev/core/src/com/google/gwt/dev/util/Util.java
+++ b/dev/core/src/com/google/gwt/dev/util/Util.java
@@ -307,12 +307,65 @@
    * Escapes '&', '<', '>', '"', and '\'' to their XML entity equivalents.
    */
   public static String escapeXml(String unescaped) {
-    String escaped = unescaped.replaceAll("\\&", "&amp;");
-    escaped = escaped.replaceAll("\\<", "&lt;");
-    escaped = escaped.replaceAll("\\>", "&gt;");
-    escaped = escaped.replaceAll("\\\"", "&quot;");
-    escaped = escaped.replaceAll("\\'", "&apos;");
-    return escaped;
+    StringBuilder builder = new StringBuilder();
+    escapeXml(unescaped, 0, unescaped.length(), true, builder);
+    return builder.toString();
+  }
+  
+  /**
+   * Escapes '&', '<', '>', '"', and optionally ''' to their XML entity
+   * equivalents. The portion of the input string between start (inclusive) and
+   * end (exclusive) is scanned.  The output is appended to the given
+   * StringBuilder.
+   * 
+   * @param code the input String
+   * @param start the first character position to scan.
+   * @param end the character position following the last character to scan.
+   * @param quoteApostrophe if true, the &apos; character is quoted as
+   *     &amp;apos;
+   * @param builder a StringBuilder to be appended with the output.
+   */
+  public static void escapeXml(String code, int start, int end,
+      boolean quoteApostrophe, StringBuilder builder) {
+    int lastIndex = 0;
+    int len = end - start;
+    char[] c = new char[len];
+    
+    code.getChars(start, end, c, 0);
+    for (int i = 0; i < len; i++) {
+      switch (c[i]) {
+        case '&':
+          builder.append(c, lastIndex, i - lastIndex);
+          builder.append("&amp;");
+          lastIndex = i + 1;
+          break;
+        case '>':
+          builder.append(c, lastIndex, i - lastIndex);
+          builder.append("&gt;");
+          lastIndex = i + 1;
+          break;
+        case '<':
+          builder.append(c, lastIndex, i - lastIndex);
+          builder.append("&lt;");
+          lastIndex = i + 1;
+          break;
+        case '\"':
+          builder.append(c, lastIndex, i - lastIndex);
+          builder.append("&quot;");
+          lastIndex = i + 1;
+          break;
+        case '\'':
+          if (quoteApostrophe) {
+            builder.append(c, lastIndex, i - lastIndex);
+            builder.append("&apos;");
+            lastIndex = i + 1;
+          }
+          break;
+        default:
+          break;
+      }
+    }
+    builder.append(c, lastIndex, len - lastIndex);
   }
 
   public static URL findSourceInClassPath(ClassLoader cl, String sourceTypeName) {
@@ -1211,6 +1264,72 @@
       Utility.close(stream);
     }
   }
+  
+  /**
+   * Writes the contents of a StringBuilder to an OutputStream, encoding
+   * each character using the UTF-* encoding.  Unicode characters between
+   * U+0000 and U+10FFFF are supported.
+   */
+  public static void writeUtf8(StringBuilder builder, OutputStream out)
+      throws IOException {
+    // Rolling our own converter avoids the following:
+    //
+    // o Instantiating the entire builder as a String
+    // o Creating CharEncoders and NIO buffer
+    // o Passing through an OutputStreamWriter
+
+    int buflen = 1024;
+    char[] inBuf = new char[buflen];
+    byte[] outBuf = new byte[4 * buflen];
+    
+    int length = builder.length();
+    int start = 0;
+
+    while (start < length) {
+      int end = Math.min(start + buflen, length);
+      builder.getChars(start, end, inBuf, 0);
+
+      int index = 0;
+      int len = end - start;
+      for (int i = 0; i < len; i++) {
+        int c = inBuf[i] & 0xffff;
+        if (c < 0x80) {
+          outBuf[index++] = (byte) c;
+        } else if (c < 0x800) {
+          int y = c >> 8;
+          int x = c & 0xff;
+          outBuf[index++] = (byte) (0xc0 | (y << 2) | (x >> 6)); // 110yyyxx
+          outBuf[index++] = (byte) (0x80 | (x & 0x3f));          // 10xxxxxx
+        } else if (c < 0xD800 || c > 0xDFFF) {
+          int y = (c >> 8) & 0xff;
+          int x = c & 0xff;
+          outBuf[index++] = (byte) (0xe0 | (y >> 4));            // 1110yyyy
+          outBuf[index++] = (byte) (0x80 | ((y << 2) & 0x3c) | (x >> 6)); // 10yyyyxx
+          outBuf[index++] = (byte) (0x80 | (x & 0x3f));          // 10xxxxxx
+        } else {
+          // Ignore if no second character (which is not be legal unicode)
+          if (i + 1 < len) {
+            int hi = c & 0x3ff;
+            int lo = inBuf[i + 1] & 0x3ff;
+
+            int full = 0x10000 + ((hi << 10) | lo);
+            int z = (full >> 16) & 0xff;
+            int y = (full >> 8) & 0xff;
+            int x = full & 0xff;
+
+            outBuf[index++] = (byte) (0xf0 | (z >> 5));
+            outBuf[index++] = (byte) (0x80 | ((z << 4) & 0x30) | (y >> 4));
+            outBuf[index++] = (byte) (0x80 | ((y << 2) & 0x3c) | (x >> 6));
+            outBuf[index++] = (byte) (0x80 | (x & 0x3f));
+            
+            i++; // char has been consumed
+          }
+        }
+      }
+      out.write(outBuf, 0, index);
+      start = end;
+    } 
+  }
 
   // /**
   // * Write all of the supplied bytes to the file, in a way that they can be
diff --git a/dev/core/test/com/google/gwt/core/ext/util/UtilSuite.java b/dev/core/test/com/google/gwt/core/ext/util/UtilSuite.java
new file mode 100644
index 0000000..291dc81
--- /dev/null
+++ b/dev/core/test/com/google/gwt/core/ext/util/UtilSuite.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2009 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.core.ext.util;
+
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+public class UtilSuite extends TestSuite {
+  public static Test suite() {
+    TestSuite suite = new TestSuite("Test suite for Util");
+    suite.addTestSuite(UtilTest.class);
+    return suite;
+  }
+}
diff --git a/dev/core/test/com/google/gwt/core/ext/util/UtilTest.java b/dev/core/test/com/google/gwt/core/ext/util/UtilTest.java
new file mode 100644
index 0000000..5b3c394
--- /dev/null
+++ b/dev/core/test/com/google/gwt/core/ext/util/UtilTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.core.ext.util;
+
+import com.google.gwt.dev.util.Util;
+
+import junit.framework.TestCase;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+
+/**
+ * Tests for some com.google.gwt.dev.util.Util methods.
+ */
+public class UtilTest extends TestCase {
+
+  public UtilTest() {
+  }
+
+  public void testExpandXml() {
+    String[] INPUTS = {
+      "ab&yoohoo<ok",
+      "ab&yoohoo<",
+      "&yoohoo<ok",
+      "ab&<ok",
+      "ab&yoohoo<ok",
+      "&",
+      "\'",
+      "\"",
+      ">",
+      "<"
+    };
+    
+    String[] OUTPUTS = {
+      "ab&amp;yoohoo&lt;ok",
+      "ab&amp;yoohoo&lt;",
+      "&amp;yoohoo&lt;ok",
+      "ab&amp;&lt;ok",
+      "ab&amp;yoohoo&lt;ok",
+      "&amp;",
+      "&apos;",
+      "&quot;",
+      "&gt;",
+      "&lt;"
+    };
+
+    for (int i = 0; i < INPUTS.length; i++) {
+      assertEquals(Util.escapeXml(INPUTS[i]), OUTPUTS[i]);
+    }
+  }
+  
+  private void testWriteUtf8(String input) throws IOException {
+    // Built-in encoder
+    ByteArrayOutputStream out1 = new ByteArrayOutputStream();
+    Writer writer = new OutputStreamWriter(out1, "UTF-8");
+    writer.write(input);
+    writer.close();
+    byte[] bytes1 = out1.toByteArray();
+    
+    // Util encoder
+    StringBuilder builder = new StringBuilder();
+    builder.append(input);
+    ByteArrayOutputStream out2 = new ByteArrayOutputStream();
+    Util.writeUtf8(builder, out2);
+    byte[] bytes2 = out2.toByteArray();
+    
+    assertEquals(bytes1.length, bytes2.length);
+    for (int i = 0; i < bytes1.length; i++) {
+      assertEquals("input = " + input + " at byte " + i, bytes1[i], bytes2[i]);
+    }
+  }
+  
+  public void testWriteUtf8() {
+    String[] INPUTS = {
+        "灰色",
+        "plain ascii text",
+        "té×ţ ŵıŦƕ lŎts œf wƎiƦd ƇhÅrȿ",
+        "ĝréý",
+        "$ᄋme סf everりthınğ งฆฅฦศ ႳႹႩႨႵ ᄃᄥᄷᄨᎤᎸᎹᎺᴓ",
+        "ᴄᴅᴈᴇἓἤἥἨὙΩ№℗℘←↑→↓✤✥✦✧�ﺕﺖꀕꀖꀗ逅逖逡遇違遝遭遷",
+        "Surrogate pairs: 𝍠𐀁𐀵𐅇𐅋𐅊𝄐𝄑𝄢𝄫𝄞𝍤𝍦𝍨再善𯾰𯿩"
+    };
+    
+    try {
+      for (String input : INPUTS) {
+        testWriteUtf8(input);
+      }
+    } catch (IOException e) {
+      fail("Got IOException");
+    }
+  }
+}