Created a new DiskCache class which caches large blocks of data on disk to converse heap.
- Generated CompilationUnits cache their source code.
- CompiledClasses cache their bytes.
Review by: jat, bobv
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@5163 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/javac/CompilationUnit.java b/dev/core/src/com/google/gwt/dev/javac/CompilationUnit.java
index b2b5469..bede4e1 100644
--- a/dev/core/src/com/google/gwt/dev/javac/CompilationUnit.java
+++ b/dev/core/src/com/google/gwt/dev/javac/CompilationUnit.java
@@ -435,13 +435,6 @@
}
/**
- * Called when this unit no longer needs to keep an internal cache of its
- * source.
- */
- protected void dumpSource() {
- }
-
- /**
* If compiled, returns all contained classes; otherwise returns
* <code>null</code>.
*/
@@ -479,7 +472,6 @@
}
void setChecked() {
- dumpSource();
assert cud != null || state == State.GRAVEYARD;
for (CompiledClass compiledClass : getCompiledClasses()) {
compiledClass.checked();
@@ -500,14 +492,12 @@
}
void setError() {
- dumpSource();
this.errors = cud.compilationResult().getErrors();
invalidate();
state = State.ERROR;
}
void setFresh() {
- dumpSource();
this.errors = null;
invalidate();
state = State.FRESH;
diff --git a/dev/core/src/com/google/gwt/dev/javac/CompiledClass.java b/dev/core/src/com/google/gwt/dev/javac/CompiledClass.java
index f3f1eec..359dd36 100644
--- a/dev/core/src/com/google/gwt/dev/javac/CompiledClass.java
+++ b/dev/core/src/com/google/gwt/dev/javac/CompiledClass.java
@@ -17,6 +17,7 @@
import com.google.gwt.core.ext.typeinfo.JRealClassType;
import com.google.gwt.dev.javac.impl.Shared;
+import com.google.gwt.dev.util.DiskCache;
import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.compiler.ClassFile;
@@ -24,7 +25,6 @@
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileReader;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFormatException;
import org.eclipse.jdt.internal.compiler.env.NameEnvironmentAnswer;
-import org.eclipse.jdt.internal.compiler.lookup.LocalTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.SourceTypeBinding;
/**
@@ -32,6 +32,8 @@
*/
public final class CompiledClass {
+ private static final DiskCache diskCache = new DiskCache();
+
private static ClassFile getClassFile(TypeDeclaration typeDecl,
String binaryName) {
for (ClassFile tryClassFile : typeDecl.compilationResult().getClassFiles()) {
@@ -45,24 +47,21 @@
return null;
}
- private static String getPackagePrefix(String packageName) {
- return packageName.length() > 0 ? packageName + "." : "";
- }
-
protected final String binaryName;
- protected final byte[] bytes;
protected final CompiledClass enclosingClass;
protected final String location;
- protected final String packageName;
- protected final String sourceName;
protected final CompilationUnit unit;
- // The state below is transient.
- private NameEnvironmentAnswer nameEnvironmentAnswer;
- private JRealClassType realClassType;
+ /**
+ * A token to retrieve this object's bytes from the disk cache.
+ */
+ private final long cacheToken;
+ // The state below is transient.
+ private transient NameEnvironmentAnswer nameEnvironmentAnswer;
+ private transient JRealClassType realClassType;
// Can be killed after parent is CHECKED.
- private TypeDeclaration typeDeclaration;
+ private transient TypeDeclaration typeDeclaration;
CompiledClass(CompilationUnit unit, TypeDeclaration typeDeclaration,
CompiledClass enclosingClass) {
@@ -71,17 +70,9 @@
this.enclosingClass = enclosingClass;
SourceTypeBinding binding = typeDeclaration.binding;
this.binaryName = CharOperation.charToString(binding.constantPoolName());
- this.packageName = Shared.getPackageNameFromBinary(binaryName);
- if (binding instanceof LocalTypeBinding) {
- // The source name of a local type must be determined from binary.
- String qualifiedName = binaryName.replace('/', '.');
- this.sourceName = qualifiedName.replace('$', '.');
- } else {
- this.sourceName = getPackagePrefix(packageName)
- + String.valueOf(binding.qualifiedSourceName());
- }
ClassFile classFile = getClassFile(typeDeclaration, binaryName);
- this.bytes = classFile.getBytes();
+ byte[] bytes = classFile.getBytes();
+ this.cacheToken = diskCache.writeByteArray(bytes);
this.location = String.valueOf(classFile.fileName());
}
@@ -96,7 +87,7 @@
* Returns the bytes of the compiled class.
*/
public byte[] getBytes() {
- return bytes;
+ return diskCache.readByteArray(cacheToken);
}
public CompiledClass getEnclosingClass() {
@@ -107,14 +98,14 @@
* Returns the enclosing package, e.g. {@code java.util}.
*/
public String getPackageName() {
- return packageName;
+ return Shared.getPackageNameFromBinary(binaryName);
}
/**
* Returns the qualified source name, e.g. {@code java.util.Map.Entry}.
*/
public String getSourceName() {
- return sourceName;
+ return binaryName.replace('/', '.').replace('$', '.');
}
public CompilationUnit getUnit() {
@@ -136,7 +127,8 @@
NameEnvironmentAnswer getNameEnvironmentAnswer() {
if (nameEnvironmentAnswer == null) {
try {
- ClassFileReader cfr = new ClassFileReader(bytes, location.toCharArray());
+ ClassFileReader cfr = new ClassFileReader(getBytes(),
+ location.toCharArray());
nameEnvironmentAnswer = new NameEnvironmentAnswer(cfr, null);
} catch (ClassFormatException e) {
throw new RuntimeException("Unexpectedly unable to parse class file", e);
diff --git a/dev/core/src/com/google/gwt/dev/javac/JsniCollector.java b/dev/core/src/com/google/gwt/dev/javac/JsniCollector.java
index 40b03cb..92251c7 100644
--- a/dev/core/src/com/google/gwt/dev/javac/JsniCollector.java
+++ b/dev/core/src/com/google/gwt/dev/javac/JsniCollector.java
@@ -133,16 +133,38 @@
}
}
+ /**
+ * Just a little class to grab source only once for a compilation unit.
+ *
+ * TODO: parse JSNI <i>during</i> JDT's cycle to avoiding having to read from
+ * disk twice.
+ */
+ private static final class SourceCache {
+ private String source;
+ private final CompilationUnit unit;
+
+ public SourceCache(CompilationUnit unit) {
+ this.unit = unit;
+ }
+
+ public String get() {
+ if (source == null) {
+ source = unit.getSource();
+ }
+ return source;
+ }
+ }
+
public static void collectJsniMethods(TreeLogger logger,
Set<CompilationUnit> units, JsProgram program) {
for (CompilationUnit unit : units) {
if (unit.getState() == State.COMPILED) {
String loc = unit.getDisplayLocation();
- String source = unit.getSource();
+ SourceCache sourceCache = new SourceCache(unit);
assert unit.getJsniMethods() == null;
List<JsniMethod> jsniMethods = new ArrayList<JsniMethod>();
for (CompiledClass compiledClass : unit.getCompiledClasses()) {
- jsniMethods.addAll(collectJsniMethods(logger, loc, source,
+ jsniMethods.addAll(collectJsniMethods(logger, loc, sourceCache,
compiledClass, program));
}
unit.setJsniMethods(jsniMethods);
@@ -154,7 +176,8 @@
* TODO: log real errors, replacing GenerateJavaScriptAST?
*/
private static List<JsniMethod> collectJsniMethods(TreeLogger logger,
- String loc, String source, CompiledClass compiledClass, JsProgram program) {
+ String loc, SourceCache sourceCache, CompiledClass compiledClass,
+ JsProgram program) {
TypeDeclaration typeDecl = compiledClass.getTypeDeclaration();
int[] lineEnds = typeDecl.compilationResult.getLineSeparatorPositions();
List<JsniMethod> jsniMethods = new ArrayList<JsniMethod>();
@@ -165,6 +188,7 @@
if (!method.isNative()) {
continue;
}
+ String source = sourceCache.get();
Interval interval = findJsniSource(source, method);
if (interval == null) {
String msg = "No JavaScript body found for native method '" + method
diff --git a/dev/core/src/com/google/gwt/dev/javac/impl/FileCompilationUnit.java b/dev/core/src/com/google/gwt/dev/javac/impl/FileCompilationUnit.java
index 568791a..d71ffec 100644
--- a/dev/core/src/com/google/gwt/dev/javac/impl/FileCompilationUnit.java
+++ b/dev/core/src/com/google/gwt/dev/javac/impl/FileCompilationUnit.java
@@ -23,7 +23,7 @@
/**
* A compilation unit based on a file.
*/
-public final class FileCompilationUnit extends CompilationUnit {
+public class FileCompilationUnit extends CompilationUnit {
private final File file;
private final String typeName;
diff --git a/dev/core/src/com/google/gwt/dev/javac/impl/SourceFileCompilationUnit.java b/dev/core/src/com/google/gwt/dev/javac/impl/SourceFileCompilationUnit.java
index c3e7c9e..6c9830e 100644
--- a/dev/core/src/com/google/gwt/dev/javac/impl/SourceFileCompilationUnit.java
+++ b/dev/core/src/com/google/gwt/dev/javac/impl/SourceFileCompilationUnit.java
@@ -23,7 +23,6 @@
*/
public class SourceFileCompilationUnit extends CompilationUnit {
- private String sourceCode;
private JavaSourceFile sourceFile;
public SourceFileCompilationUnit(JavaSourceFile sourceFile) {
@@ -42,10 +41,7 @@
@Override
public String getSource() {
- if (sourceCode == null) {
- sourceCode = sourceFile.readSource();
- }
- return sourceCode;
+ return sourceFile.readSource();
}
public JavaSourceFile getSourceFile() {
@@ -66,10 +62,4 @@
public boolean isSuperSource() {
return sourceFile.isSuperSource();
}
-
- @Override
- protected void dumpSource() {
- sourceCode = null;
- }
-
}
diff --git a/dev/core/src/com/google/gwt/dev/shell/StandardGeneratorContext.java b/dev/core/src/com/google/gwt/dev/shell/StandardGeneratorContext.java
index 34b10a2..6037d6b 100644
--- a/dev/core/src/com/google/gwt/dev/shell/StandardGeneratorContext.java
+++ b/dev/core/src/com/google/gwt/dev/shell/StandardGeneratorContext.java
@@ -29,14 +29,18 @@
import com.google.gwt.dev.cfg.PublicOracle;
import com.google.gwt.dev.javac.CompilationState;
import com.google.gwt.dev.javac.CompilationUnit;
-import com.google.gwt.dev.javac.impl.Shared;
+import com.google.gwt.dev.javac.impl.FileCompilationUnit;
+import com.google.gwt.dev.util.DiskCache;
import com.google.gwt.dev.util.Util;
import com.google.gwt.dev.util.collect.HashSet;
import com.google.gwt.dev.util.collect.IdentityHashMap;
import java.io.ByteArrayOutputStream;
import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
import java.io.OutputStream;
+import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
@@ -52,47 +56,57 @@
public class StandardGeneratorContext implements GeneratorContext {
/**
+ * Extras added to {@link CompilationUnit}.
+ */
+ interface Generated {
+ void abort();
+
+ void commit();
+
+ String getTypeName();
+ }
+
+ /**
* This compilation unit acts as a normal compilation unit as well as a buffer
* into which generators can write their source. A controller should ensure
* that source isn't requested until the generator has finished writing it.
+ * This version is backed by {@link StandardGeneratorContext#diskCache}.
*/
- private static class GeneratedUnitWithFile extends CompilationUnit {
+ private static class GeneratedUnit extends CompilationUnit implements
+ Generated {
+
+ /**
+ * A token to retrieve this object's bytes from the disk cache.
+ */
+ private long cacheToken;
private long creationTime;
- private File file;
-
- private PrintWriter pw;
-
- private String source;
-
private StringWriter sw;
private final String typeName;
- public GeneratedUnitWithFile(String typeName) {
+ public GeneratedUnit(StringWriter sw, String typeName) {
this.typeName = typeName;
- sw = new StringWriter();
- pw = new PrintWriter(sw, true);
+ this.sw = sw;
+ }
+
+ public void abort() {
+ sw = null;
}
/**
* Finalizes the source and adds this compilation unit to the host.
*/
public void commit() {
- source = sw.toString();
- pw = null;
+ cacheToken = diskCache.writeString(sw.toString());
sw = null;
creationTime = System.currentTimeMillis();
}
@Override
public String getDisplayLocation() {
- if (file == null) {
- return "transient source for " + typeName;
- } else {
- return file.getAbsoluteFile().toURI().toString();
- }
+ return "transient source for " + typeName;
}
@Override
@@ -102,14 +116,10 @@
@Override
public String getSource() {
- if (source == null && file == null) {
+ if (sw != null) {
throw new IllegalStateException("source not committed");
}
- if (source == null) {
- source = Util.readFileAsString(file);
- }
- assert (source != null);
- return source;
+ return diskCache.readString(cacheToken);
}
@Override
@@ -122,26 +132,55 @@
return true;
}
- public boolean isOnDisk() {
- return file != null;
+ @Override
+ public boolean isSuperSource() {
+ return false;
+ }
+ }
+
+ /**
+ * This compilation unit acts as a normal compilation unit as well as a buffer
+ * into which generators can write their source. A controller should ensure
+ * that source isn't requested until the generator has finished writing it.
+ * This version is backed by an explicit generated file.
+ */
+ private static class GeneratedUnitWithFile extends FileCompilationUnit
+ implements Generated {
+
+ private PrintWriter pw;
+
+ public GeneratedUnitWithFile(File file, PrintWriter pw, String packageName) {
+ super(file, packageName);
+ this.pw = pw;
+ }
+
+ public void abort() {
+ pw.close();
+ pw = null;
+ }
+
+ public void commit() {
+ pw.close();
+ pw = null;
+ }
+
+ @Override
+ public String getSource() {
+ if (pw != null) {
+ throw new IllegalStateException("source not committed");
+ }
+ return super.getSource();
+ }
+
+ @Override
+ public boolean isGenerated() {
+ return true;
}
@Override
public boolean isSuperSource() {
return false;
}
-
- public void setFile(File file) {
- assert (file.exists() && file.canRead());
- this.file = file;
- }
-
- @Override
- protected void dumpSource() {
- if (file != null) {
- source = null;
- }
- }
}
/**
@@ -178,9 +217,11 @@
}
}
+ private static DiskCache diskCache;
+
private final ArtifactSet allGeneratedArtifacts;
- private final Set<GeneratedUnitWithFile> committedGeneratedCups = new HashSet<GeneratedUnitWithFile>();
+ private final Set<CompilationUnit> committedGeneratedCups = new HashSet<CompilationUnit>();
private final CompilationState compilationState;
@@ -200,7 +241,7 @@
private final PublicOracle publicOracle;
- private final Map<PrintWriter, GeneratedUnitWithFile> uncommittedGeneratedCupsByPrintWriter = new IdentityHashMap<PrintWriter, GeneratedUnitWithFile>();
+ private final Map<PrintWriter, Generated> uncommittedGeneratedCupsByPrintWriter = new IdentityHashMap<PrintWriter, Generated>();
/**
* Normally, the compiler host would be aware of the same types that are
@@ -215,17 +256,20 @@
this.genDir = genDir;
this.generatorResourcesDir = generatorResourcesDir;
this.allGeneratedArtifacts = allGeneratedArtifacts;
+ if (genDir == null && diskCache == null) {
+ diskCache = new DiskCache();
+ }
}
/**
* Commits a pending generated type.
*/
public final void commit(TreeLogger logger, PrintWriter pw) {
- GeneratedUnitWithFile gcup = uncommittedGeneratedCupsByPrintWriter.get(pw);
+ Generated gcup = uncommittedGeneratedCupsByPrintWriter.get(pw);
if (gcup != null) {
gcup.commit();
uncommittedGeneratedCupsByPrintWriter.remove(pw);
- committedGeneratedCups.add(gcup);
+ committedGeneratedCups.add((CompilationUnit) gcup);
} else {
logger.log(TreeLogger.WARN,
"Generator attempted to commit an unknown PrintWriter", null);
@@ -302,11 +346,9 @@
"Generated source files...", null);
}
- for (GeneratedUnitWithFile gcup : committedGeneratedCups) {
+ for (CompilationUnit gcup : committedGeneratedCups) {
String qualifiedTypeName = gcup.getTypeName();
genTypeNames.add(qualifiedTypeName);
- maybeWriteSource(gcup, qualifiedTypeName);
-
if (subBranch != null) {
subBranch.log(TreeLogger.DEBUG, gcup.getDisplayLocation(), null);
}
@@ -333,7 +375,7 @@
String msg = "For the following type(s), generated source was never committed (did you forget to call commit()?)";
logger = logger.branch(TreeLogger.WARN, msg, null);
- for (GeneratedUnitWithFile unit : uncommittedGeneratedCupsByPrintWriter.values()) {
+ for (Generated unit : uncommittedGeneratedCupsByPrintWriter.values()) {
logger.log(TreeLogger.WARN, unit.getTypeName(), null);
}
}
@@ -359,8 +401,12 @@
public final PrintWriter tryCreate(TreeLogger logger, String packageName,
String simpleTypeName) {
- String typeName = packageName + "." + simpleTypeName;
-
+ String typeName;
+ if (packageName.length() == 0) {
+ typeName = simpleTypeName;
+ } else {
+ typeName = packageName + '.' + simpleTypeName;
+ }
// Is type already known to the host?
JClassType existingType = getTypeOracle().findType(packageName,
simpleTypeName);
@@ -381,17 +427,34 @@
// The type isn't there, so we can let the caller create it. Remember that
// it is pending so another attempt to create the same type will fail.
- String qualifiedSourceName;
- if (packageName.length() == 0) {
- qualifiedSourceName = simpleTypeName;
+ Generated gcup;
+ PrintWriter pw;
+ if (this.genDir == null) {
+ StringWriter sw = new StringWriter();
+ pw = new PrintWriter(sw, true);
+ gcup = new GeneratedUnit(sw, typeName);
} else {
- qualifiedSourceName = packageName + '.' + simpleTypeName;
+ File dir = new File(genDir, packageName.replace('.', File.separatorChar));
+ dir.mkdirs();
+ File srcFile = new File(dir, simpleTypeName + ".java");
+ if (srcFile.exists()) {
+ srcFile.delete();
+ }
+ try {
+ FileOutputStream fos = new FileOutputStream(srcFile);
+ // Critical to set the encoding here, or UTF chars get whacked.
+ OutputStreamWriter osw = new OutputStreamWriter(fos,
+ Util.DEFAULT_ENCODING);
+ pw = new PrintWriter(osw);
+ gcup = new GeneratedUnitWithFile(srcFile, pw, packageName);
+ } catch (IOException e) {
+ throw new RuntimeException("Error writing out generated unit at '"
+ + srcFile.getAbsolutePath() + "'", e);
+ }
}
- GeneratedUnitWithFile gcup = new GeneratedUnitWithFile(qualifiedSourceName);
- uncommittedGeneratedCupsByPrintWriter.put(gcup.pw, gcup);
+ uncommittedGeneratedCupsByPrintWriter.put(pw, gcup);
newlyGeneratedTypeNames.add(typeName);
-
- return gcup.pw;
+ return pw;
}
public OutputStream tryCreateResource(TreeLogger logger, String partialPath)
@@ -483,30 +546,4 @@
pendingResourcesByOutputStream.clear();
}
}
-
- /**
- * Writes the source of the specified compilation unit to disk if a gen
- * directory is specified.
- *
- * @param unit the compilation unit whose contents might need to be written
- * @param qualifiedTypeName the fully-qualified type name
- */
- private void maybeWriteSource(GeneratedUnitWithFile unit,
- String qualifiedTypeName) {
-
- if (unit.isOnDisk() || genDir == null) {
- // No place to write it.
- return;
- }
-
- // Let's do write it.
- String packageName = Shared.getPackageName(qualifiedTypeName);
- String shortName = Shared.getShortName(qualifiedTypeName);
- File dir = new File(genDir, packageName.replace('.', File.separatorChar));
- dir.mkdirs();
- File srcFile = new File(dir, shortName + ".java");
- if (Util.writeStringAsFile(srcFile, unit.getSource())) {
- unit.setFile(srcFile);
- }
- }
}
diff --git a/dev/core/src/com/google/gwt/dev/util/DiskCache.java b/dev/core/src/com/google/gwt/dev/util/DiskCache.java
new file mode 100644
index 0000000..c1bd2b5
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/util/DiskCache.java
@@ -0,0 +1,141 @@
+/*
+ * 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.dev.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A nifty class that lets you squirrel away data on the file system. Write
+ * once, read many times. Instance of this are thread-safe by way of internal
+ * synchronization.
+ *
+ * Note that in the current implementation, the backing temp file will get
+ * arbitrarily large as you continue adding things to it. There is no internal
+ * GC or compaction.
+ */
+public class DiskCache {
+ /**
+ * For future thought: if we used Object tokens instead of longs, we could
+ * actually track references and do GC/compaction on the underlying file.
+ *
+ * I considered using memory mapping, but I didn't see any obvious way to make
+ * the map larger after the fact, which kind of defeats the infinite-append
+ * design. At any rate, I measured the current performance of this design to
+ * be so fast relative to what I'm using it for, I didn't pursue this further.
+ */
+
+ private static class Shutdown implements Runnable {
+ public void run() {
+ for (DiskCache diskCache : shutdownList) {
+ try {
+ diskCache.finalize();
+ } catch (Throwable e) {
+ }
+ }
+ }
+ }
+
+ private static List<DiskCache> shutdownList;
+
+ private boolean atEnd = true;
+ private RandomAccessFile file;
+
+ public DiskCache() {
+ try {
+ File temp = File.createTempFile("gwt", "byte-cache");
+ temp.deleteOnExit();
+ file = new RandomAccessFile(temp, "rw");
+ file.setLength(0);
+ if (shutdownList == null) {
+ shutdownList = new ArrayList<DiskCache>();
+ Runtime.getRuntime().addShutdownHook(new Thread(new Shutdown()));
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Unable to initialize byte cache", e);
+ }
+ }
+
+ /**
+ * Read some bytes off disk.
+ *
+ * @param token a handle previously returned from
+ * {@link #writeByteArray(byte[])}
+ * @return the bytes that were written
+ */
+ public byte[] readByteArray(long token) {
+ try {
+ atEnd = false;
+ file.seek(token);
+ int length = file.readInt();
+ byte[] result = new byte[length];
+ file.readFully(result);
+ return result;
+ } catch (IOException e) {
+ throw new RuntimeException("Unable to read from byte cache", e);
+ }
+ }
+
+ /**
+ * Read a String from disk.
+ *
+ * @param token a handle previously returned from {@link #writeString(String)}
+ * @return the String that was written
+ */
+ public String readString(long token) {
+ return Util.toString(readByteArray(token));
+ }
+
+ /**
+ * Write a byte array to disk.
+ *
+ * @return a handle to retrieve it later
+ */
+ public long writeByteArray(byte[] bytes) {
+ try {
+ if (!atEnd) {
+ file.seek(file.length());
+ }
+ long position = file.getFilePointer();
+ file.writeInt(bytes.length);
+ file.write(bytes);
+ return position;
+ } catch (IOException e) {
+ throw new RuntimeException("Unable to write to byte cache", e);
+ }
+ }
+
+ /**
+ * Write a String to disk.
+ *
+ * @return a handle to retrieve it later
+ */
+ public long writeString(String str) {
+ return writeByteArray(Util.getBytes(str));
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ if (file != null) {
+ file.setLength(0);
+ file.close();
+ file = null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/dev/core/test/com/google/gwt/dev/util/DiskCacheTest.java b/dev/core/test/com/google/gwt/dev/util/DiskCacheTest.java
new file mode 100644
index 0000000..36c0ec9
--- /dev/null
+++ b/dev/core/test/com/google/gwt/dev/util/DiskCacheTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.dev.util;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+
+/**
+ * Tests {@link DiskCache}.
+ */
+public class DiskCacheTest extends TestCase {
+ private final DiskCache diskCache = new DiskCache();
+
+ public void testBytes() {
+ byte[] a = new byte[0];
+ byte[] b = new byte[] {1, 5, 9, 7, 3, 4, 2};
+ byte[] c = new byte[3524];
+ for (int i = 1; i < c.length; ++i) {
+ c[i] = (byte) (i * 31 + c[i - 1]);
+ }
+ byte[][] insertOrder = new byte[][] {a, b, c, b, c, a, a, b, b, c, c, a};
+ long[] tokens = new long[insertOrder.length];
+ for (int i = 0; i < insertOrder.length; ++i) {
+ tokens[i] = diskCache.writeByteArray(insertOrder[i]);
+ }
+
+ int testIndex = 0;
+ for (int i = 0; i < 20; ++i) {
+ testIndex += tokens[i % tokens.length];
+ testIndex %= insertOrder.length;
+ byte[] expected = insertOrder[testIndex];
+ byte[] actual = diskCache.readByteArray(tokens[testIndex]);
+ assertTrue("Values were not equals at index '" + testIndex + "'",
+ Arrays.equals(expected, actual));
+ }
+ }
+
+ public void testStrings() {
+ String a = "";
+ String b = "abjdsfkl;jasdf";
+ char[] c = new char[2759];
+ for (int i = 1; i < c.length; ++i) {
+ c[i] = (char) (i * 31 + c[i - 1]);
+ // Avoid problematic characters.
+ c[i] &= 0x7FFF;
+ --c[i];
+ }
+ String s = String.valueOf(c);
+ String[] insertOrder = new String[] {s, a, b, s, b, s, a, a, s, b, b, a, s};
+ long[] tokens = new long[insertOrder.length];
+ for (int i = 0; i < insertOrder.length; ++i) {
+ tokens[i] = diskCache.writeString(insertOrder[i]);
+ }
+
+ int testIndex = 0;
+ for (int i = 0; i < 20; ++i) {
+ testIndex += tokens[i % tokens.length];
+ testIndex %= insertOrder.length;
+ String expected = insertOrder[testIndex];
+ String actual = diskCache.readString(tokens[testIndex]);
+ assertEquals("Values were not equals at index '" + testIndex + "'",
+ expected, actual);
+ }
+ }
+}