classlib: implement WeakHashMap

Fix #799
This commit is contained in:
Alexey Andreev 2024-04-02 21:40:34 +02:00
parent f668e27daa
commit e069bc3a44
8 changed files with 1125 additions and 65 deletions

View File

@ -22,6 +22,11 @@ public final class PlatformDetector {
private PlatformDetector() { private PlatformDetector() {
} }
@PlatformMarker
public static boolean isTeaVM() {
return false;
}
@PlatformMarker(Platforms.WEBASSEMBLY) @PlatformMarker(Platforms.WEBASSEMBLY)
public static boolean isWebAssembly() { public static boolean isWebAssembly() {
return false; return false;

View File

@ -0,0 +1,596 @@
/*
* Copyright 2024 Alexey Andreev.
*
* 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.
*/
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.teavm.classlib.java.util;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.ConcurrentModificationException;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
public class TWeakHashMap<K, V> extends TAbstractMap<K, V> implements TMap<K, V> {
private static final int DEFAULT_SIZE = 16;
private final ReferenceQueue<K> referenceQueue;
private int elementCount;
private Entry<K, V>[] elementData;
private final int loadFactor;
private int threshold;
private int modCount;
// Simple utility method to isolate unchecked cast for array creation
@SuppressWarnings("unchecked")
private static <K, V> Entry<K, V>[] newEntryArray(int size) {
return new Entry[size];
}
private static final class Entry<K, V> extends WeakReference<K> implements TMap.Entry<K, V> {
int hash;
boolean isNull;
V value;
Entry<K, V> next;
interface Type<R, K, V> {
R get(TMap.Entry<K, V> entry);
}
Entry(K key, V object, ReferenceQueue<K> queue) {
super(key, queue);
isNull = key == null;
hash = isNull ? 0 : key.hashCode();
value = object;
}
@Override
public K getKey() {
return super.get();
}
@Override
public V getValue() {
return value;
}
@Override
public V setValue(V object) {
V result = value;
value = object;
return result;
}
@Override
public boolean equals(Object other) {
if (!(other instanceof Map.Entry)) {
return false;
}
var entry = (Map.Entry<?, ?>) other;
Object key = super.get();
return Objects.equals(key, entry.getKey()) && Objects.equals(value, entry.getValue());
}
@Override
public int hashCode() {
return hash ^ Objects.hashCode(value);
}
@Override
public String toString() {
return super.get() + "=" + value;
}
}
class HashIterator<R> implements TIterator<R> {
private int position;
private int expectedModCount;
private Entry<K, V> currentEntry;
private Entry<K, V> nextEntry;
private K nextKey;
final Entry.Type<R, K, V> type;
HashIterator(Entry.Type<R, K, V> type) {
this.type = type;
expectedModCount = modCount;
}
@Override
public boolean hasNext() {
if (nextEntry != null && (nextKey != null || nextEntry.isNull)) {
return true;
}
while (true) {
if (nextEntry == null) {
while (position < elementData.length) {
nextEntry = elementData[position++];
if (nextEntry != null) {
break;
}
}
if (nextEntry == null) {
return false;
}
}
// ensure key of next entry is not gc'ed
nextKey = nextEntry.get();
if (nextKey != null || nextEntry.isNull) {
return true;
}
nextEntry = nextEntry.next;
}
}
@Override
public R next() {
if (expectedModCount == modCount) {
if (hasNext()) {
currentEntry = nextEntry;
nextEntry = currentEntry.next;
R result = type.get(currentEntry);
// free the key
nextKey = null;
return result;
}
throw new NoSuchElementException();
}
throw new ConcurrentModificationException();
}
@Override
public void remove() {
if (expectedModCount == modCount) {
if (currentEntry != null) {
removeEntry(currentEntry);
currentEntry = null;
expectedModCount++;
// cannot poll() as that would change the expectedModCount
} else {
throw new IllegalStateException();
}
} else {
throw new ConcurrentModificationException();
}
}
}
public TWeakHashMap() {
this(DEFAULT_SIZE);
}
public TWeakHashMap(int capacity) {
if (capacity >= 0) {
elementCount = 0;
elementData = newEntryArray(capacity == 0 ? 1 : capacity);
loadFactor = 7500; // Default load factor of 0.75
computeMaxSize();
referenceQueue = new ReferenceQueue<>();
} else {
throw new IllegalArgumentException();
}
}
public TWeakHashMap(int capacity, float loadFactor) {
if (capacity >= 0 && loadFactor > 0) {
elementCount = 0;
elementData = newEntryArray(capacity == 0 ? 1 : capacity);
this.loadFactor = (int) (loadFactor * 10000);
computeMaxSize();
referenceQueue = new ReferenceQueue<>();
} else {
throw new IllegalArgumentException();
}
}
public TWeakHashMap(TMap<? extends K, ? extends V> map) {
this(map.size() < 6 ? 11 : map.size() * 2);
putAllImpl(map);
}
@Override
public void clear() {
if (elementCount > 0) {
elementCount = 0;
Arrays.fill(elementData, null);
modCount++;
while (referenceQueue.poll() != null) {
// do nothing
}
}
}
private void computeMaxSize() {
threshold = (int) ((long) elementData.length * loadFactor / 10000);
}
@Override
public boolean containsKey(Object key) {
return getEntry(key) != null;
}
@Override
public TSet<TMap.Entry<K, V>> entrySet() {
poll();
return new TAbstractSet<>() {
@Override
public int size() {
return TWeakHashMap.this.size();
}
@Override
public void clear() {
TWeakHashMap.this.clear();
}
@Override
public boolean remove(Object object) {
if (contains(object)) {
TWeakHashMap.this.remove(((Map.Entry<?, ?>) object).getKey());
return true;
}
return false;
}
@Override
public boolean contains(Object object) {
if (object instanceof TMap.Entry) {
var entry = getEntry(((TMap.Entry<?, ?>) object).getKey());
if (entry != null) {
Object key = entry.get();
if (key != null || entry.isNull) {
return object.equals(entry);
}
}
}
return false;
}
@Override
public TIterator<TMap.Entry<K, V>> iterator() {
return new HashIterator<>(entry -> entry);
}
};
}
@Override
public TSet<K> keySet() {
poll();
if (cachedKeySet == null) {
cachedKeySet = new TAbstractSet<K>() {
@Override
public boolean contains(Object object) {
return containsKey(object);
}
@Override
public int size() {
return TWeakHashMap.this.size();
}
@Override
public void clear() {
TWeakHashMap.this.clear();
}
@Override
public boolean remove(Object key) {
if (containsKey(key)) {
TWeakHashMap.this.remove(key);
return true;
}
return false;
}
@Override
public TIterator<K> iterator() {
return new HashIterator<>(TMap.Entry::getKey);
}
@Override
public Object[] toArray() {
var coll = new TArrayList<K>(size());
for (var iter = iterator(); iter.hasNext();) {
coll.add(iter.next());
}
return coll.toArray();
}
@Override
public <T> T[] toArray(T[] contents) {
var coll = new ArrayList<K>(size());
for (var iter = iterator(); iter.hasNext();) {
coll.add(iter.next());
}
return coll.toArray(contents);
}
};
}
return cachedKeySet;
}
@Override
public TCollection<V> values() {
poll();
if (cachedValues == null) {
cachedValues = new TAbstractCollection<V>() {
@Override
public int size() {
return TWeakHashMap.this.size();
}
@Override
public void clear() {
TWeakHashMap.this.clear();
}
@Override
public boolean contains(Object object) {
return containsValue(object);
}
@Override
public TIterator<V> iterator() {
return new HashIterator<>(TMap.Entry::getValue);
}
};
}
return cachedValues;
}
@Override
public V get(Object key) {
poll();
if (key != null) {
int index = (key.hashCode() & 0x7FFFFFFF) % elementData.length;
var entry = elementData[index];
while (entry != null) {
if (key.equals(entry.get())) {
return entry.value;
}
entry = entry.next;
}
return null;
}
var entry = elementData[0];
while (entry != null) {
if (entry.isNull) {
return entry.value;
}
entry = entry.next;
}
return null;
}
private Entry<K, V> getEntry(Object key) {
poll();
if (key != null) {
int index = (key.hashCode() & 0x7FFFFFFF) % elementData.length;
Entry<K, V> entry = elementData[index];
while (entry != null) {
if (key.equals(entry.get())) {
return entry;
}
entry = entry.next;
}
return null;
}
Entry<K, V> entry = elementData[0];
while (entry != null) {
if (entry.isNull) {
return entry;
}
entry = entry.next;
}
return null;
}
@Override
public boolean containsValue(Object value) {
poll();
if (value != null) {
for (int i = elementData.length; --i >= 0;) {
var entry = elementData[i];
while (entry != null) {
K key = entry.get();
if ((key != null || entry.isNull) && value.equals(entry.value)) {
return true;
}
entry = entry.next;
}
}
} else {
for (int i = elementData.length; --i >= 0;) {
var entry = elementData[i];
while (entry != null) {
K key = entry.get();
if ((key != null || entry.isNull) && entry.value == null) {
return true;
}
entry = entry.next;
}
}
}
return false;
}
@Override
public boolean isEmpty() {
return size() == 0;
}
@SuppressWarnings("unchecked")
private void poll() {
Entry<K, V> toRemove;
while ((toRemove = (Entry<K, V>) referenceQueue.poll()) != null) {
removeEntry(toRemove);
}
}
private void removeEntry(Entry<K, V> toRemove) {
Entry<K, V> entry;
Entry<K, V> last = null;
int index = (toRemove.hash & 0x7FFFFFFF) % elementData.length;
entry = elementData[index];
// Ignore queued entries which cannot be found, the user could
// have removed them before they were queued, i.e. using clear()
while (entry != null) {
if (toRemove == entry) {
modCount++;
if (last == null) {
elementData[index] = entry.next;
} else {
last.next = entry.next;
}
elementCount--;
break;
}
last = entry;
entry = entry.next;
}
}
@Override
public V put(K key, V value) {
poll();
int index = 0;
Entry<K, V> entry;
if (key != null) {
index = (key.hashCode() & 0x7FFFFFFF) % elementData.length;
entry = elementData[index];
while (entry != null && !key.equals(entry.get())) {
entry = entry.next;
}
} else {
entry = elementData[0];
while (entry != null && !entry.isNull) {
entry = entry.next;
}
}
if (entry == null) {
modCount++;
if (++elementCount > threshold) {
rehash();
index = key == null ? 0 : (key.hashCode() & 0x7FFFFFFF) % elementData.length;
}
entry = new Entry<>(key, value, referenceQueue);
entry.next = elementData[index];
elementData[index] = entry;
return null;
}
V result = entry.value;
entry.value = value;
return result;
}
private void rehash() {
int length = elementData.length << 1;
if (length == 0) {
length = 1;
}
Entry<K, V>[] newData = newEntryArray(length);
for (var elementDatum : elementData) {
var entry = elementDatum;
while (entry != null) {
int index = entry.isNull ? 0 : (entry.hash & 0x7FFFFFFF) % length;
var next = entry.next;
entry.next = newData[index];
newData[index] = entry;
entry = next;
}
}
elementData = newData;
computeMaxSize();
}
@Override
public void putAll(TMap<? extends K, ? extends V> map) {
putAllImpl(map);
}
/**
* Removes the mapping with the specified key from this map.
*
* @param key
* the key of the mapping to remove.
* @return the value of the removed mapping or {@code null} if no mapping
* for the specified key was found.
*/
@Override
public V remove(Object key) {
poll();
int index = 0;
Entry<K, V> entry;
Entry<K, V> last = null;
if (key != null) {
index = (key.hashCode() & 0x7FFFFFFF) % elementData.length;
entry = elementData[index];
while (entry != null && !key.equals(entry.get())) {
last = entry;
entry = entry.next;
}
} else {
entry = elementData[0];
while (entry != null && !entry.isNull) {
last = entry;
entry = entry.next;
}
}
if (entry != null) {
modCount++;
if (last == null) {
elementData[index] = entry.next;
} else {
last.next = entry.next;
}
elementCount--;
return entry.value;
}
return null;
}
@Override
public int size() {
poll();
return elementCount;
}
private void putAllImpl(TMap<? extends K, ? extends V> map) {
if (map.entrySet() != null) {
super.putAll(map);
}
}
}

View File

@ -16,9 +16,9 @@
function init(target, queue) { function init(target, queue) {
let supported = typeof teavm_globals.WeakRef !== 'undefined'; let supported = typeof teavm_globals.WeakRef !== 'undefined';
let value = supported ? new teavm_globals.WeakRef(target) : target; let value = supported && target !== null ? new teavm_globals.WeakRef(target) : target;
this[teavm_javaField("java.lang.ref.WeakReference", "value")] = value; this[teavm_javaField("java.lang.ref.WeakReference", "value")] = value;
if (queue !== null && supported) { if (queue !== null && supported && target !== null) {
let registry = queue[teavm_javaField("java.lang.ref.ReferenceQueue", "registry")]; let registry = queue[teavm_javaField("java.lang.ref.ReferenceQueue", "registry")];
if (registry !== null) { if (registry !== null) {
registry.register(target, this); registry.register(target, this);

View File

@ -58,9 +58,13 @@ public class WeakReferenceDependencyListener extends AbstractDependencyListener
private void queueMethodReached(DependencyAgent agent, MethodDependency method) { private void queueMethodReached(DependencyAgent agent, MethodDependency method) {
switch (method.getMethod().getName()) { switch (method.getMethod().getName()) {
case "poll": case "poll": {
var reportMethod = agent.linkMethod(new MethodReference(ReferenceQueue.class,
"reportNext", Reference.class, boolean.class));
initRef.connect(method.getResult()); initRef.connect(method.getResult());
reportMethod.use();
break; break;
}
case "<init>": case "<init>":
agent.linkField(new FieldReference(ReferenceQueue.class.getName(), "inner")); agent.linkField(new FieldReference(ReferenceQueue.class.getName(), "inner"));
agent.linkField(new FieldReference(ReferenceQueue.class.getName(), "registry")); agent.linkField(new FieldReference(ReferenceQueue.class.getName(), "registry"));

View File

@ -15,66 +15,62 @@
*/ */
package org.teavm.classlib.java.lang.ref; package org.teavm.classlib.java.lang.ref;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame; import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import java.lang.ref.ReferenceQueue; import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ArrayBlockingQueue;
import org.junit.Ignore; import java.util.concurrent.TimeUnit;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.teavm.classlib.PlatformDetector;
import org.teavm.classlib.support.GCSupport;
import org.teavm.junit.EachTestCompiledSeparately; import org.teavm.junit.EachTestCompiledSeparately;
import org.teavm.junit.SkipJVM; import org.teavm.junit.SkipPlatform;
import org.teavm.junit.TeaVMTestRunner; import org.teavm.junit.TeaVMTestRunner;
import org.teavm.junit.TestPlatform;
@RunWith(TeaVMTestRunner.class) @RunWith(TeaVMTestRunner.class)
@SkipJVM
@EachTestCompiledSeparately @EachTestCompiledSeparately
public class WeakReferenceTest { public class WeakReferenceTest {
private Node lastNode;
@Test @Test
@Ignore @SkipPlatform({ TestPlatform.JAVASCRIPT, TestPlatform.WEBASSEMBLY, TestPlatform.WASI })
public void deref() throws InterruptedException { public void deref() {
var ref = createAndTestRef(null); var ref = createAndTestRef(null);
GCSupport.tryToTriggerGC(ref);
for (var i = 0; i < 100; ++i) {
lastNode = createNodes(20);
Thread.sleep(1);
if (ref.get() == null) {
break;
}
assertNotNull(lastNode);
}
assertNull(ref.get()); assertNull(ref.get());
} }
@Test @Test
@Ignore @SkipPlatform({ TestPlatform.JAVASCRIPT, TestPlatform.WEBASSEMBLY, TestPlatform.WASI })
public void refQueue() throws InterruptedException { public void refQueue() {
var queue = new ReferenceQueue<>(); var queue = new ReferenceQueue<>();
var ref = createAndTestRef(queue); var ref = createAndTestRef(queue);
var hasValue = false; GCSupport.tryToTriggerGC(ref);
for (var i = 0; i < 100; ++i) { int attemptCount = 0;
lastNode = createNodes(20); Object value;
Thread.sleep(1); do {
var polledRef = queue.poll(); value = queue.poll();
if (polledRef != null) { if (value != null) {
hasValue = true;
assertNull(ref.get());
break; break;
} else {
assertNotNull(ref.get());
} }
assertNotNull(lastNode); waitInJVM();
} while (attemptCount++ < 50);
assertSame(ref, value);
}
private static void waitInJVM() {
if (!PlatformDetector.isTeaVM()) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// do nothing
}
} }
assertTrue(hasValue);
} }
@Test @Test
@Ignore @SkipPlatform({ TestPlatform.C, TestPlatform.WEBASSEMBLY, TestPlatform.WASI })
public void queueRemove() throws InterruptedException { public void queueRemove() throws InterruptedException {
var queue = new ReferenceQueue<>(); var queue = new ReferenceQueue<>();
var ref = createAndTestRef(queue); var ref = createAndTestRef(queue);
@ -88,17 +84,10 @@ public class WeakReferenceTest {
}); });
thread.setDaemon(true); thread.setDaemon(true);
thread.start(); thread.start();
Object value = null;
for (var i = 0; i < 100; ++i) { GCSupport.tryToTriggerGC(ref);
lastNode = createNodes(20); var result = threadQueue.poll(2, TimeUnit.SECONDS);
Thread.sleep(1); assertSame(ref, result);
value = threadQueue.poll();
if (value != null) {
break;
}
assertNotNull(lastNode);
}
assertSame(ref, value);
} }
private WeakReference<Object> createAndTestRef(ReferenceQueue<Object> queue) { private WeakReference<Object> createAndTestRef(ReferenceQueue<Object> queue) {
@ -117,22 +106,4 @@ public class WeakReferenceTest {
ref.clear(); ref.clear();
assertNull(ref.get()); assertNull(ref.get());
} }
private Node createNodes(int depth) {
if (depth == 0) {
return null;
} else {
return new Node(createNodes(depth - 1), createNodes(depth - 1));
}
}
private class Node {
Node left;
Node right;
Node(Node left, Node right) {
this.left = left;
this.right = right;
}
}
} }

View File

@ -0,0 +1,385 @@
/*
* Copyright 2024 Alexey Andreev.
*
* 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.
*/
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.teavm.classlib.java.util;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import java.util.AbstractMap;
import java.util.Arrays;
import java.util.Set;
import java.util.WeakHashMap;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.teavm.classlib.support.GCSupport;
import org.teavm.classlib.support.MapTest2Support;
import org.teavm.junit.SkipPlatform;
import org.teavm.junit.TeaVMTestRunner;
import org.teavm.junit.TestPlatform;
@RunWith(TeaVMTestRunner.class)
@SkipPlatform({TestPlatform.WEBASSEMBLY, TestPlatform.WASI})
public class WeakHashMapTest {
static class MockMap<K, V> extends AbstractMap<K, V> {
@Override
public Set<Entry<K, V>> entrySet() {
return null;
}
@Override
public int size() {
return 0;
}
}
Object[] keyArray = new Object[100];
Object[] valueArray = new Object[100];
WeakHashMap<Object, Object> whm;
@Test
public void constructor() {
new MapTest2Support(new WeakHashMap<>()).runTest();
whm = new WeakHashMap<>();
for (int i = 0; i < 100; i++) {
whm.put(keyArray[i], valueArray[i]);
}
for (int i = 0; i < 100; i++) {
assertSame("Incorrect value retrieved", valueArray[i], whm.get(keyArray[i]));
}
}
@Test
public void constructorI() {
whm = new WeakHashMap<>(50);
for (int i = 0; i < 100; i++) {
whm.put(keyArray[i], valueArray[i]);
}
for (int i = 0; i < 100; i++) {
assertSame("Incorrect value retrieved", valueArray[i], whm.get(keyArray[i]));
}
var empty = new WeakHashMap<>(0);
assertNull("Empty weakhashmap access", empty.get("nothing"));
empty.put("something", "here");
assertSame("cannot get element", "here", empty.get("something"));
}
@Test
public void constructorIF() {
whm = new WeakHashMap<>(50, 0.5f);
for (int i = 0; i < 100; i++) {
whm.put(keyArray[i], valueArray[i]);
}
for (int i = 0; i < 100; i++) {
assertSame("Incorrect value retrieved", valueArray[i], whm.get(keyArray[i]));
}
var empty = new WeakHashMap<>(0, 0.75f);
assertNull("Empty hashtable access", empty.get("nothing"));
empty.put("something", "here");
assertSame("cannot get element", "here", empty.get("something"));
}
@Test
public void constructorLjava_util_Map() {
var map = new WeakHashMap<>(new MockMap<>());
assertEquals("Size should be 0", 0, map.size());
}
@Test
public void clearMethod() {
whm = new WeakHashMap<>();
for (int i = 0; i < 100; i++) {
whm.put(keyArray[i], valueArray[i]);
}
whm.clear();
assertTrue("Cleared map should be empty", whm.isEmpty());
for (int i = 0; i < 100; i++) {
assertNull("Cleared map should only return null", whm.get(keyArray[i]));
}
}
@Test
public void containsKey() {
whm = new WeakHashMap<>();
for (int i = 0; i < 100; i++) {
whm.put(keyArray[i], valueArray[i]);
}
for (int i = 0; i < 100; i++) {
assertTrue("Should contain referenced key", whm.containsKey(keyArray[i]));
}
keyArray[25] = null;
keyArray[50] = null;
}
@Test
public void containsValue() {
whm = new WeakHashMap<>();
for (int i = 0; i < 100; i++) {
whm.put(keyArray[i], valueArray[i]);
}
for (int i = 0; i < 100; i++) {
assertTrue("Should contain referenced value", whm.containsValue(valueArray[i]));
}
keyArray[25] = null;
keyArray[50] = null;
}
@Test
public void entrySet() {
var weakMap = new WeakHashMap<>();
for (int i = 0; i < 100; i++) {
weakMap.put(keyArray[i], valueArray[i]);
}
var keys = Arrays.asList(keyArray);
var values = Arrays.asList(valueArray);
// Check the entry set has correct size & content
var entrySet = weakMap.entrySet();
assertEquals("Assert 0: Incorrect number of entries returned", 100, entrySet.size());
var it = entrySet.iterator();
while (it.hasNext()) {
var entry = it.next();
assertTrue("Assert 1: Invalid map entry key returned", keys.contains(entry.getKey()));
assertTrue("Assert 2: Invalid map entry value returned", values.contains(entry.getValue()));
assertTrue("Assert 3: Entry not in entry set", entrySet.contains(entry));
}
// Dereference a single key, then try to
// force a collection of the weak ref'd obj
keyArray[50] = null;
GCSupport.tryToTriggerGC();
assertEquals("Assert 4: Incorrect number of entries after gc", 99, entrySet.size());
assertSame("Assert 5: Entries not identical", entrySet.iterator().next(), entrySet.iterator().next());
// remove alternate entries using the iterator, and ensure the
// iteration count is consistent
int size = entrySet.size();
it = entrySet.iterator();
while (it.hasNext()) {
it.next();
it.remove();
size--;
if (it.hasNext()) {
it.next();
}
}
assertEquals("Assert 6: entry set count mismatch", size, entrySet.size());
int entries = 0;
it = entrySet.iterator();
while (it.hasNext()) {
it.next();
entries++;
}
assertEquals("Assert 6: count mismatch", size, entries);
it = entrySet.iterator();
while (it.hasNext()) {
it.next();
it.remove();
}
assertEquals("Assert 7: entry set not empty", 0, entrySet.size());
assertFalse("Assert 8: iterator not empty", entrySet.iterator().hasNext());
}
@Test
public void entrySet2() {
whm = new WeakHashMap<>();
for (int i = 0; i < 100; i++) {
whm.put(keyArray[i], valueArray[i]);
}
var keys = Arrays.asList(keyArray);
var values = Arrays.asList(valueArray);
var entrySet = whm.entrySet();
assertEquals("Incorrect number of entries returned--wanted 100, got: " + entrySet.size(),
100, entrySet.size());
for (var entry : entrySet) {
assertTrue("Invalid map entry returned--bad key", keys.contains(entry.getKey()));
assertTrue("Invalid map entry returned--bad key", values.contains(entry.getValue()));
}
keys = null;
values = null;
keyArray[50] = null;
GCSupport.tryToTriggerGC();
assertEquals("Incorrect number of entries returned after gc--wanted 99, got: " + entrySet.size(),
99, entrySet.size());
}
@Test
public void get() {
assertTrue("Used to test", true);
}
@Test
public void isEmpty() {
whm = new WeakHashMap<>();
assertTrue("New map should be empty", whm.isEmpty());
Object myObject = new Object();
whm.put(myObject, myObject);
assertFalse("Map should not be empty", whm.isEmpty());
whm.remove(myObject);
assertTrue("Map with elements removed should be empty", whm.isEmpty());
}
@Test
public void put() {
var map = new WeakHashMap<>();
map.put(null, "value"); // add null key
GCSupport.tryToTriggerGC();
map.remove("nothing"); // Cause objects in queue to be removed
assertEquals("null key was removed", 1, map.size());
}
@Test
public void putAll() {
var mockMap = new MockMap<>();
var map = new WeakHashMap<>();
map.putAll(mockMap);
assertEquals("Size should be 0", 0, map.size());
}
@Test
public void remove() {
whm = new WeakHashMap<>();
for (int i = 0; i < 100; i++) {
whm.put(keyArray[i], valueArray[i]);
}
assertSame("Remove returned incorrect value", valueArray[25], whm.remove(keyArray[25]));
assertNull("Remove returned incorrect value", whm.remove(keyArray[25]));
assertEquals("Size should be 99 after remove", 99, whm.size());
}
@Test
public void size() {
assertTrue("Used to test", true);
}
@Test
public void keySet() {
whm = new WeakHashMap<>();
for (int i = 0; i < 100; i++) {
whm.put(keyArray[i], valueArray[i]);
}
var keys = Arrays.asList(keyArray);
var values = Arrays.asList(valueArray);
var keySet = whm.keySet();
assertEquals("Incorrect number of keys returned,", 100, keySet.size());
for (var key : keySet) {
assertTrue("Invalid map entry returned--bad key", keys.contains(key));
}
keys = null;
values = null;
keyArray[50] = null;
GCSupport.tryToTriggerGC();
assertEquals("Incorrect number of keys returned after gc,", 99, keySet.size());
}
@Test
public void keySetHasNext() {
var map = new WeakHashMap<>();
var cl = new ConstantHashClass(2);
map.put(new ConstantHashClass(1), null);
map.put(cl, null);
map.put(new ConstantHashClass(3), null);
var iter = map.keySet().iterator();
iter.next();
iter.next();
GCSupport.tryToTriggerGC();
assertFalse("Wrong hasNext() value", iter.hasNext());
}
static class ConstantHashClass {
private int id;
public ConstantHashClass(int id) {
this.id = id;
}
public int hashCode() {
return 0;
}
public String toString() {
return "ConstantHashClass[id=" + id + "]";
}
}
@Test
public void values() {
whm = new WeakHashMap<>();
for (int i = 0; i < 100; i++) {
whm.put(keyArray[i], valueArray[i]);
}
var keys = Arrays.asList(keyArray);
var values = Arrays.asList(valueArray);
var valuesCollection = whm.values();
assertEquals("Incorrect number of keys returned,", 100, valuesCollection.size());
for (Object value : valuesCollection) {
assertTrue("Invalid map entry returned--bad value", values.contains(value));
}
keys = null;
values = null;
keyArray[50] = null;
GCSupport.tryToTriggerGC();
assertEquals("Incorrect number of keys returned after gc", 99, valuesCollection.size());
}
@Before
public void setUp() {
for (int i = 0; i < 100; i++) {
keyArray[i] = new Object();
valueArray[i] = new Object();
}
}
}

View File

@ -0,0 +1,98 @@
/*
* Copyright 2024 konsoletyper.
*
* 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 org.teavm.classlib.support;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import org.teavm.classlib.PlatformDetector;
import org.teavm.interop.Async;
import org.teavm.interop.AsyncCallback;
import org.teavm.jso.JSBody;
import org.teavm.jso.browser.Window;
import org.teavm.jso.dom.html.HTMLDocument;
public final class GCSupport {
private GCSupport() {
}
public static void tryToTriggerGC() {
tryToTriggerGC(null);
}
public static void tryToTriggerGC(Reference<?> ref) {
if (PlatformDetector.isC() || PlatformDetector.isWebAssembly()) {
System.gc();
return;
}
var weakReferences = new ArrayList<WeakReference<Object>>();
for (var i = 0; i < 100; ++i) {
System.out.println("GC trigger attempt " + i);
weakReferences.add(new WeakReference<>(generateTree("R")));
waitInJS();
if (weakReferences.stream().anyMatch(s -> s.get() == null)) {
if (ref != null) {
if (ref.get() == null) {
break;
}
} else if (i > 5) {
break;
}
}
}
}
private static void waitInJS() {
if (PlatformDetector.isJavaScript()) {
var doc = HTMLDocument.current();
var div = doc.createElement("div");
div.appendChild(doc.createTextNode("hello"));
doc.getBody().appendChild(div);
triggerGCInJS();
waitImpl();
triggerGCInJS();
waitImpl();
} else {
Runtime.getRuntime().gc();
Runtime.getRuntime().gc();
}
}
@Async
private static native void waitImpl();
private static void waitImpl(AsyncCallback<Void> callback) {
Window.setTimeout(() -> callback.complete(null), 0);
}
@JSBody(script = "if (typeof window.gc === 'function') { window.gc(); }")
private static native void triggerGCInJS();
private static Tree generateTree(String path) {
var result = new Tree();
result.s = path;
if (path.length() < 18) {
result.a = generateTree(path + "l");
result.b = generateTree(path + "r");
}
return result;
}
private static class Tree {
String s;
Tree a;
Tree b;
}
}

View File

@ -420,6 +420,7 @@ public class BrowserRunner {
"--disable-gpu", "--disable-gpu",
"--remote-debugging-port=9222", "--remote-debugging-port=9222",
"--no-first-run", "--no-first-run",
"--js-flags=--expose-gc",
"--user-data-dir=" + profile "--user-data-dir=" + profile
)); ));
}); });