Optimize the output production by reusing 16k buffers over and over.

By reusing over and over a buffer small enough to fit into the processor's L2 cache, we realize a huge savings on data transfer.  This replaces the previous method of reading data entirely into huge byte[] and then writing it back in one shot.  Experimentally, this is shown to be much faster.

Review by: spoon, fabbott

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@5404 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/EmittedArtifact.java b/dev/core/src/com/google/gwt/core/ext/linker/EmittedArtifact.java
index a0a71c4..4bb578a 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/EmittedArtifact.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/EmittedArtifact.java
@@ -19,10 +19,11 @@
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.dev.util.Util;
+import com.google.gwt.util.tools.Utility;
 
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 
 /**
  * An artifact that will be emitted into the output. All EmittedArtifacts
@@ -50,20 +51,6 @@
   }
 
   /**
-   * Provides access to the contents of the EmittedResource as a byte buffer.
-   */
-  public byte[] getBytes(TreeLogger logger) throws UnableToCompleteException {
-    ByteArrayOutputStream baos = new ByteArrayOutputStream();
-    try {
-      Util.copy(getContents(logger), baos);
-    } catch (IOException e) {
-      logger.log(TreeLogger.ERROR, "Unable to read stream", e);
-      throw new UnableToCompleteException();
-    }
-    return baos.toByteArray();
-  }
-
-  /**
    * Provides access to the contents of the EmittedResource.
    */
   public abstract InputStream getContents(TreeLogger logger)
@@ -126,6 +113,21 @@
     return getPartialPath();
   }
 
+  /**
+   * Provides access to the contents of the EmittedResource.
+   */
+  public void writeTo(TreeLogger logger, OutputStream out)
+      throws UnableToCompleteException {
+    try {
+      InputStream in = getContents(logger);
+      Util.copyNoClose(in, out);
+      Utility.close(in);
+    } catch (IOException e) {
+      logger.log(TreeLogger.ERROR, "Unable to read or write stream", e);
+      throw new UnableToCompleteException();
+    }
+  }
+
   @Override
   protected final int compareToComparableArtifact(EmittedArtifact o) {
     return getPartialPath().compareTo(o.getPartialPath());
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/SyntheticArtifact.java b/dev/core/src/com/google/gwt/core/ext/linker/SyntheticArtifact.java
index 7158bf5..9faa554 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/SyntheticArtifact.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/SyntheticArtifact.java
@@ -22,6 +22,7 @@
 
 import java.io.ByteArrayInputStream;
 import java.io.InputStream;
+import java.io.OutputStream;
 
 /**
  * Artifacts created by {@link AbstractLinker}.
@@ -46,11 +47,6 @@
   }
 
   @Override
-  public byte[] getBytes(TreeLogger logger) throws UnableToCompleteException {
-    return diskCache.readByteArray(token);
-  }
-
-  @Override
   public InputStream getContents(TreeLogger logger)
       throws UnableToCompleteException {
     return new ByteArrayInputStream(diskCache.readByteArray(token));
@@ -60,4 +56,10 @@
   public long getLastModified() {
     return lastModified;
   }
+
+  @Override
+  public void writeTo(TreeLogger logger, OutputStream out)
+      throws UnableToCompleteException {
+    diskCache.writeTo(token, out);
+  }
 }
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardGeneratedResource.java b/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardGeneratedResource.java
index f2aae42..c27a7af 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardGeneratedResource.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardGeneratedResource.java
@@ -24,7 +24,6 @@
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.RandomAccessFile;
 
 /**
  * The standard implementation of {@link GeneratedResource}.
@@ -39,19 +38,6 @@
   }
 
   @Override
-  public byte[] getBytes(TreeLogger logger) throws UnableToCompleteException {
-    try {
-      RandomAccessFile raf = new RandomAccessFile(file, "r");
-      byte[] buf = new byte[(int) raf.length()];
-      raf.readFully(buf);
-      return buf;
-    } catch (IOException e) {
-      logger.log(TreeLogger.ERROR, "Unable to read file", e);
-      throw new UnableToCompleteException();
-    }
-  }
-
-  @Override
   public InputStream getContents(TreeLogger logger)
       throws UnableToCompleteException {
     try {
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 2100ce0..fdebdbe 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
@@ -55,8 +55,8 @@
 import com.google.gwt.dev.util.Util;
 
 import java.io.File;
+import java.io.FileOutputStream;
 import java.io.IOException;
-import java.io.RandomAccessFile;
 import java.io.Reader;
 import java.io.StringReader;
 import java.util.ArrayList;
@@ -491,13 +491,11 @@
           || (outFile.lastModified() <= artifact.getLastModified())) {
         mkdirs(outFile.getParentFile(), createdDirs);
         try {
-          RandomAccessFile raf = new RandomAccessFile(outFile, "rw");
-          byte[] bytes = artifact.getBytes(artifactLogger);
-          raf.setLength(bytes.length);
-          raf.write(bytes);
-          raf.close();
+          FileOutputStream out = new FileOutputStream(outFile);
+          artifact.writeTo(artifactLogger, out);
+          out.close();
         } catch (IOException e) {
-          logger.log(TreeLogger.ERROR, "Unable to create file '"
+          artifactLogger.log(TreeLogger.ERROR, "Unable to create file '"
               + outFile.getAbsolutePath() + "'", e);
           throw new UnableToCompleteException();
         }
@@ -529,10 +527,6 @@
       Class<? extends Linker> linkerType) {
     assert linkerShortNames.containsKey(linkerType) : linkerType.getName()
         + " unknown";
-    File toReturn = new File(extraPath, linkerShortNames.get(linkerType));
-    if (!toReturn.exists()) {
-      toReturn.mkdirs();
-    }
-    return toReturn;
+    return new File(extraPath, linkerShortNames.get(linkerType));
   }
 }
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardPublicResource.java b/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardPublicResource.java
index 09e79a7..eecad1e 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardPublicResource.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardPublicResource.java
@@ -25,6 +25,7 @@
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.io.Serializable;
 
 /**
@@ -47,11 +48,6 @@
     }
 
     @Override
-    public byte[] getBytes(TreeLogger logger) throws UnableToCompleteException {
-      return data;
-    }
-
-    @Override
     public InputStream getContents(TreeLogger logger)
         throws UnableToCompleteException {
       return new ByteArrayInputStream(data);
@@ -61,6 +57,17 @@
     public long getLastModified() {
       return lastModified;
     }
+
+    @Override
+    public void writeTo(TreeLogger logger, OutputStream out)
+        throws UnableToCompleteException {
+      try {
+        out.write(data);
+      } catch (IOException e) {
+        logger.log(TreeLogger.ERROR, "Unable to write to stream", e);
+        throw new UnableToCompleteException();
+      }
+    }
   }
 
   private final Resource resource;
diff --git a/dev/core/src/com/google/gwt/dev/util/DiskCache.java b/dev/core/src/com/google/gwt/dev/util/DiskCache.java
index 34568ea..0b5f991 100644
--- a/dev/core/src/com/google/gwt/dev/util/DiskCache.java
+++ b/dev/core/src/com/google/gwt/dev/util/DiskCache.java
@@ -19,6 +19,7 @@
 import java.io.ByteArrayOutputStream;

 import java.io.File;

 import java.io.IOException;

+import java.io.OutputStream;

 import java.io.RandomAccessFile;

 import java.lang.ref.WeakReference;

 import java.util.ArrayList;

@@ -86,7 +87,7 @@
    *          {@link #writeByteArray(byte[])}

    * @return the bytes that were written

    */

-  public byte[] readByteArray(long token) {

+  public synchronized byte[] readByteArray(long token) {

     try {

       atEnd = false;

       file.seek(token);

@@ -128,7 +129,7 @@
    * 

    * @return a handle to retrieve it later

    */

-  public long writeByteArray(byte[] bytes) {

+  public synchronized long writeByteArray(byte[] bytes) {

     try {

       if (!atEnd) {

         file.seek(file.length());

@@ -162,8 +163,36 @@
     return writeByteArray(Util.getBytes(str));

   }

 

+  /**

+   * Reads bytes of data back from disk and writes them into the specified

+   * output stream.

+   */

+  public synchronized void writeTo(long token, OutputStream out) {

+    byte[] buf = Util.takeThreadLocalBuf();

+    try {

+      atEnd = false;

+      file.seek(token);

+      int length = file.readInt();

+      int bufLen = buf.length;

+      while (length > bufLen) {

+        int read = file.read(buf, 0, bufLen);

+        length -= read;

+        out.write(buf, 0, read);

+      }

+      while (length > 0) {

+        int read = file.read(buf, 0, length);

+        length -= read;

+        out.write(buf, 0, read);

+      }

+    } catch (IOException e) {

+      throw new RuntimeException("Unable to read from byte cache", e);

+    } finally {

+      Util.releaseThreadLocalBuf(buf);

+    }

+  }

+

   @Override

-  protected void finalize() throws Throwable {

+  protected synchronized void finalize() throws Throwable {

     if (file != null) {

       file.setLength(0);

       file.close();

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 1ce3b0a..8631513 100644
--- a/dev/core/src/com/google/gwt/dev/util/Util.java
+++ b/dev/core/src/com/google/gwt/dev/util/Util.java
@@ -83,6 +83,18 @@
       '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D',
       'E', 'F'};
 
+  /**
+   * The size of a {@link #threadLocalBuf}, which should be large enough for
+   * efficient data transfer but small enough to fit easily into the L2 cache of
+   * most modern processors.
+   */
+  private static final int THREAD_LOCAL_BUF_SIZE = 16 * 1024;
+
+  /**
+   * Stores reusable thread local buffers for efficient data transfer.
+   */
+  private static final ThreadLocal<byte[]> threadLocalBuf = new ThreadLocal<byte[]>();
+
   public static byte[] append(byte[] xs, byte x) {
     int n = xs.length;
     byte[] t = new byte[n + 1];
@@ -165,11 +177,7 @@
 
   public static void copy(InputStream is, OutputStream os) throws IOException {
     try {
-      byte[] buf = new byte[32 * 1024];
-      int i;
-      while ((i = is.read(buf)) != -1) {
-        os.write(buf, 0, i);
-      }
+      copyNoClose(is, os);
     } finally {
       Utility.close(is);
       Utility.close(os);
@@ -238,6 +246,23 @@
     }
   }
 
+  /**
+   * Copies all of the bytes from the input stream to the output stream until
+   * the input stream is EOF. Does not close either stream.
+   */
+  public static void copyNoClose(InputStream is, OutputStream os)
+      throws IOException {
+    byte[] buf = takeThreadLocalBuf();
+    try {
+      int i;
+      while ((i = is.read(buf)) != -1) {
+        os.write(buf, 0, i);
+      }
+    } finally {
+      releaseThreadLocalBuf(buf);
+    }
+  }
+
   public static Reader createReader(TreeLogger logger, URL url)
       throws UnableToCompleteException {
     try {
@@ -277,7 +302,7 @@
     }
   }
 
-/**
+  /**
    * Escapes '&', '<', '>', '"', and '\'' to their XML entity equivalents.
    */
   public static String escapeXml(String unescaped) {
@@ -817,6 +842,15 @@
     return toReturn;
   }
 
+  /**
+   * Release a buffer previously returned from {@link #takeThreadLocalBuf()}.
+   * The released buffer may then be reused.
+   */
+  public static void releaseThreadLocalBuf(byte[] buf) {
+    assert buf.length == THREAD_LOCAL_BUF_SIZE;
+    threadLocalBuf.set(buf);
+  }
+
   public static File removeExtension(File file) {
     String name = file.getName();
     int lastDot = name.lastIndexOf('.');
@@ -859,6 +893,24 @@
   }
 
   /**
+   * Get a large byte buffer local to this thread. Currently this is set to a
+   * 16k buffer, which is small enough to fit into the L2 cache on modern
+   * processors. The contents of the returned buffer are undefined. Calling
+   * {@link #releaseThreadLocalBuf(byte[])} on the returned buffer allows
+   * subsequent callers to reuse the buffer later, avoiding unncessary
+   * allocations and GC.
+   */
+  public static byte[] takeThreadLocalBuf() {
+    byte[] buf = threadLocalBuf.get();
+    if (buf == null) {
+      buf = new byte[THREAD_LOCAL_BUF_SIZE];
+    } else {
+      threadLocalBuf.set(null);
+    }
+    return buf;
+  }
+
+  /**
    * Creates an array from a collection of the specified component type and
    * size. You can definitely downcast the result to T[] if T is the specified
    * component type.