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(); + } +}