Switch to internal implementation of StringInterner to avoid class loader
leaks when reloading the page in dev mode. (Not tuned.)

Review at http://gwt-code-reviews.appspot.com/1578804

Review by: tobyr@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@10725 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/util/StringInterner.java b/dev/core/src/com/google/gwt/dev/util/StringInterner.java
index e193293..106e0e9 100644
--- a/dev/core/src/com/google/gwt/dev/util/StringInterner.java
+++ b/dev/core/src/com/google/gwt/dev/util/StringInterner.java
@@ -16,28 +16,73 @@
 
 package com.google.gwt.dev.util;
 
-import com.google.gwt.thirdparty.guava.common.collect.Interner;
-import com.google.gwt.thirdparty.guava.common.collect.Interners;
+import java.lang.ref.WeakReference;
+import java.util.WeakHashMap;
 
 /**
- * A utility class for reducing String memory waste. Note that this does not use
- * the String.intern() method which would prevent GC and fill the PermGen space.
- * Instead, we use a Google Collections WeakInterner.
+ * A utility class for reducing String memory waste.
+ *
+ * <p> We don't use the String.intern() method because it would prevent GC and fill the PermGen
+ * space. We also don't use Guava (for now) due to a class loader GC issue with the old
+ * version of Guava we're using. </p>
+ *
+ * <p> Thread-safe. The implementation uses shards to reduce thread contention. </p>
  */
 public class StringInterner {
+  // chosen via performance testing with AWFE. (See AWFE load times speadsheet.)
+  private static int SHARD_BITS = 10;
+
+  static int SHARD_COUNT = 1 << SHARD_BITS;
+  private static int SHARD_MASK = SHARD_COUNT - 1;
+
   private static final StringInterner instance = new StringInterner();
 
   public static StringInterner get() {
     return instance;
   }
 
-  private final Interner<String> stringPool = Interners.newWeakInterner();
+  private final Shard[] shards = new Shard[SHARD_COUNT];
 
   protected StringInterner() {
+    for (int i = 0; i < SHARD_COUNT; i++) {
+      shards[i] = new Shard();
+    }
   }
 
-  public String intern(String s) {
-    return stringPool.intern(s);
+  /**
+   * Returns a string equal to the input string, but not always the same.
+   */
+  public String intern(String original) {
+    int hashCode = original.hashCode();
+    int shardId = (hashCode ^ (hashCode >> SHARD_BITS)) & SHARD_MASK;
+    return shards[shardId].intern(original);
   }
 
+  /**
+   * Returns the size of each shard, for testing shard balance.
+   */
+  int[] getShardSizes() {
+    int[] result = new int[SHARD_COUNT];
+    for (int i = 0; i < shards.length; i++) {
+      result[i] = shards[i].map.size();
+    }
+    return result;
+  }
+
+  private static class Shard {
+    private final WeakHashMap<String, WeakReference<String>> map =
+        new WeakHashMap<String, WeakReference<String>>();
+
+    private synchronized String intern(String original) {
+      WeakReference<String> ref = map.get(original);
+      String interned = ref == null ? null : ref.get();
+      if (interned == null) {
+        ref = new WeakReference<String>(original);
+        map.put(original, ref);
+        return original;
+      } else {
+        return interned;
+      }
+    }
+  }
 }
diff --git a/dev/core/test/com/google/gwt/dev/util/StringInternerTest.java b/dev/core/test/com/google/gwt/dev/util/StringInternerTest.java
new file mode 100644
index 0000000..c17104a
--- /dev/null
+++ b/dev/core/test/com/google/gwt/dev/util/StringInternerTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2011 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;
+
+/**
+ * Verifies that we can intern strings.
+ *
+ * @author skybrian@google.com (Brian Slesinsky)
+ */
+public class StringInternerTest extends TestCase {
+  private StringInterner interner;
+
+  protected void setUp() throws Exception {
+    super.setUp();
+    interner = StringInterner.get();
+  }
+
+  public void testCanInternString() throws Exception {
+    String firstIn = "fnord123";
+    String secondIn = new String(firstIn);
+    assertFalse(firstIn == secondIn);
+
+    String firstOut = interner.intern(firstIn);
+    String secondOut = interner.intern(secondIn);
+    assertSame(firstOut, secondOut);
+  }
+
+  public void testCanFreeString() throws Exception {
+    // Intern about a gigabyte of data.
+    // If we don't free any interned strings, we should run out of memory.
+    // (Verified that it fails using Interns.newStrongInterner().)
+    String prefix = repeat('a', 1000);
+    for (int i = 0; i < 1000 * 1000; i++) {
+      interner.intern(prefix + i);
+    }
+  }
+
+  public void testShardsAreSomewhatBalanced() throws Exception {
+    for (int i = 0; i < 1000000; i++) {
+      interner.intern("foo" + i);
+    }
+
+    int[] shardSizes = interner.getShardSizes();
+    assertEquals(StringInterner.SHARD_COUNT, shardSizes.length);
+
+    int minShardSize = Integer.MAX_VALUE;
+    int maxShardSize = 0;
+    int total = 0;
+    for (int candidate : shardSizes) {
+      minShardSize = Math.min(minShardSize, candidate);
+      maxShardSize = Math.max(maxShardSize, candidate);
+      total += candidate;
+    }
+
+    int meanShardSize = total / shardSizes.length;
+    int expectedMinShardSize = meanShardSize / 5;
+    int expectedMaxShardSize = meanShardSize * 5;
+    if (minShardSize < expectedMinShardSize || maxShardSize > expectedMaxShardSize) {
+      fail("unbalanced shards: [" + minShardSize + "," + maxShardSize + "] not within [" +
+      expectedMinShardSize + ", " + expectedMaxShardSize + "]");
+    }
+  }
+
+  private static String repeat(char c, int length) {
+    StringBuilder buffer = new StringBuilder(length);
+    for (int i = 0; i < length; i++) {
+      buffer.append(c);
+    }
+    return buffer.toString();
+  }
+}