From e069bc3a44416788f57022e2f8ffe6a1443b782d Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Tue, 2 Apr 2024 21:40:34 +0200 Subject: [PATCH] classlib: implement WeakHashMap Fix #799 --- .../org/teavm/classlib/PlatformDetector.java | 5 + .../classlib/java/util/TWeakHashMap.java | 596 ++++++++++++++++++ .../classlib/java/lang/ref/WeakReference.js | 4 +- .../ref/WeakReferenceDependencyListener.java | 6 +- .../java/lang/ref/WeakReferenceTest.java | 95 +-- .../classlib/java/util/WeakHashMapTest.java | 385 +++++++++++ .../org/teavm/classlib/support/GCSupport.java | 98 +++ .../teavm/browserrunner/BrowserRunner.java | 1 + 8 files changed, 1125 insertions(+), 65 deletions(-) create mode 100644 classlib/src/main/java/org/teavm/classlib/java/util/TWeakHashMap.java create mode 100644 tests/src/test/java/org/teavm/classlib/java/util/WeakHashMapTest.java create mode 100644 tests/src/test/java/org/teavm/classlib/support/GCSupport.java diff --git a/classlib/src/main/java/org/teavm/classlib/PlatformDetector.java b/classlib/src/main/java/org/teavm/classlib/PlatformDetector.java index f634b0626..88abf06f7 100644 --- a/classlib/src/main/java/org/teavm/classlib/PlatformDetector.java +++ b/classlib/src/main/java/org/teavm/classlib/PlatformDetector.java @@ -22,6 +22,11 @@ public final class PlatformDetector { private PlatformDetector() { } + @PlatformMarker + public static boolean isTeaVM() { + return false; + } + @PlatformMarker(Platforms.WEBASSEMBLY) public static boolean isWebAssembly() { return false; diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/TWeakHashMap.java b/classlib/src/main/java/org/teavm/classlib/java/util/TWeakHashMap.java new file mode 100644 index 000000000..65472ecdd --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/util/TWeakHashMap.java @@ -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 extends TAbstractMap implements TMap { + private static final int DEFAULT_SIZE = 16; + + private final ReferenceQueue referenceQueue; + private int elementCount; + private Entry[] 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 Entry[] newEntryArray(int size) { + return new Entry[size]; + } + + private static final class Entry extends WeakReference implements TMap.Entry { + int hash; + boolean isNull; + V value; + Entry next; + + interface Type { + R get(TMap.Entry entry); + } + + Entry(K key, V object, ReferenceQueue 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 implements TIterator { + private int position; + private int expectedModCount; + + private Entry currentEntry; + private Entry nextEntry; + + private K nextKey; + + final Entry.Type type; + + HashIterator(Entry.Type 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 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> 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> iterator() { + return new HashIterator<>(entry -> entry); + } + }; + } + + @Override + public TSet keySet() { + poll(); + if (cachedKeySet == null) { + cachedKeySet = new TAbstractSet() { + @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 iterator() { + return new HashIterator<>(TMap.Entry::getKey); + } + + @Override + public Object[] toArray() { + var coll = new TArrayList(size()); + + for (var iter = iterator(); iter.hasNext();) { + coll.add(iter.next()); + } + return coll.toArray(); + } + + @Override + public T[] toArray(T[] contents) { + var coll = new ArrayList(size()); + + for (var iter = iterator(); iter.hasNext();) { + coll.add(iter.next()); + } + return coll.toArray(contents); + } + }; + } + return cachedKeySet; + } + + @Override + public TCollection values() { + poll(); + if (cachedValues == null) { + cachedValues = new TAbstractCollection() { + @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 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 getEntry(Object key) { + poll(); + if (key != null) { + int index = (key.hashCode() & 0x7FFFFFFF) % elementData.length; + Entry entry = elementData[index]; + while (entry != null) { + if (key.equals(entry.get())) { + return entry; + } + entry = entry.next; + } + return null; + } + Entry 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 toRemove; + while ((toRemove = (Entry) referenceQueue.poll()) != null) { + removeEntry(toRemove); + } + } + + private void removeEntry(Entry toRemove) { + Entry entry; + Entry 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 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[] 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 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 entry; + Entry 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 map) { + if (map.entrySet() != null) { + super.putAll(map); + } + } +} diff --git a/classlib/src/main/resources/org/teavm/classlib/java/lang/ref/WeakReference.js b/classlib/src/main/resources/org/teavm/classlib/java/lang/ref/WeakReference.js index c6a23f526..fdb5c2a91 100644 --- a/classlib/src/main/resources/org/teavm/classlib/java/lang/ref/WeakReference.js +++ b/classlib/src/main/resources/org/teavm/classlib/java/lang/ref/WeakReference.js @@ -16,9 +16,9 @@ function init(target, queue) { 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; - if (queue !== null && supported) { + if (queue !== null && supported && target !== null) { let registry = queue[teavm_javaField("java.lang.ref.ReferenceQueue", "registry")]; if (registry !== null) { registry.register(target, this); diff --git a/core/src/main/java/org/teavm/backend/javascript/intrinsics/ref/WeakReferenceDependencyListener.java b/core/src/main/java/org/teavm/backend/javascript/intrinsics/ref/WeakReferenceDependencyListener.java index f49982931..ed1f4885c 100644 --- a/core/src/main/java/org/teavm/backend/javascript/intrinsics/ref/WeakReferenceDependencyListener.java +++ b/core/src/main/java/org/teavm/backend/javascript/intrinsics/ref/WeakReferenceDependencyListener.java @@ -58,9 +58,13 @@ public class WeakReferenceDependencyListener extends AbstractDependencyListener private void queueMethodReached(DependencyAgent agent, MethodDependency method) { 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()); + reportMethod.use(); break; + } case "": agent.linkField(new FieldReference(ReferenceQueue.class.getName(), "inner")); agent.linkField(new FieldReference(ReferenceQueue.class.getName(), "registry")); diff --git a/tests/src/test/java/org/teavm/classlib/java/lang/ref/WeakReferenceTest.java b/tests/src/test/java/org/teavm/classlib/java/lang/ref/WeakReferenceTest.java index 3c8e36c41..e162f0952 100644 --- a/tests/src/test/java/org/teavm/classlib/java/lang/ref/WeakReferenceTest.java +++ b/tests/src/test/java/org/teavm/classlib/java/lang/ref/WeakReferenceTest.java @@ -15,66 +15,62 @@ */ package org.teavm.classlib.java.lang.ref; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; import java.util.concurrent.ArrayBlockingQueue; -import org.junit.Ignore; +import java.util.concurrent.TimeUnit; import org.junit.Test; 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.SkipJVM; +import org.teavm.junit.SkipPlatform; import org.teavm.junit.TeaVMTestRunner; +import org.teavm.junit.TestPlatform; @RunWith(TeaVMTestRunner.class) -@SkipJVM @EachTestCompiledSeparately public class WeakReferenceTest { - private Node lastNode; - @Test - @Ignore - public void deref() throws InterruptedException { + @SkipPlatform({ TestPlatform.JAVASCRIPT, TestPlatform.WEBASSEMBLY, TestPlatform.WASI }) + public void deref() { var ref = createAndTestRef(null); - - for (var i = 0; i < 100; ++i) { - lastNode = createNodes(20); - Thread.sleep(1); - if (ref.get() == null) { - break; - } - assertNotNull(lastNode); - } + GCSupport.tryToTriggerGC(ref); assertNull(ref.get()); } @Test - @Ignore - public void refQueue() throws InterruptedException { + @SkipPlatform({ TestPlatform.JAVASCRIPT, TestPlatform.WEBASSEMBLY, TestPlatform.WASI }) + public void refQueue() { var queue = new ReferenceQueue<>(); var ref = createAndTestRef(queue); - var hasValue = false; - for (var i = 0; i < 100; ++i) { - lastNode = createNodes(20); - Thread.sleep(1); - var polledRef = queue.poll(); - if (polledRef != null) { - hasValue = true; - assertNull(ref.get()); + GCSupport.tryToTriggerGC(ref); + int attemptCount = 0; + Object value; + do { + value = queue.poll(); + if (value != null) { 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 - @Ignore + @SkipPlatform({ TestPlatform.C, TestPlatform.WEBASSEMBLY, TestPlatform.WASI }) public void queueRemove() throws InterruptedException { var queue = new ReferenceQueue<>(); var ref = createAndTestRef(queue); @@ -88,17 +84,10 @@ public class WeakReferenceTest { }); thread.setDaemon(true); thread.start(); - Object value = null; - for (var i = 0; i < 100; ++i) { - lastNode = createNodes(20); - Thread.sleep(1); - value = threadQueue.poll(); - if (value != null) { - break; - } - assertNotNull(lastNode); - } - assertSame(ref, value); + + GCSupport.tryToTriggerGC(ref); + var result = threadQueue.poll(2, TimeUnit.SECONDS); + assertSame(ref, result); } private WeakReference createAndTestRef(ReferenceQueue queue) { @@ -117,22 +106,4 @@ public class WeakReferenceTest { ref.clear(); 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; - } - } } diff --git a/tests/src/test/java/org/teavm/classlib/java/util/WeakHashMapTest.java b/tests/src/test/java/org/teavm/classlib/java/util/WeakHashMapTest.java new file mode 100644 index 000000000..bbe8c09af --- /dev/null +++ b/tests/src/test/java/org/teavm/classlib/java/util/WeakHashMapTest.java @@ -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 extends AbstractMap { + @Override + public Set> entrySet() { + return null; + } + @Override + public int size() { + return 0; + } + } + + Object[] keyArray = new Object[100]; + Object[] valueArray = new Object[100]; + WeakHashMap 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(); + } + } + +} diff --git a/tests/src/test/java/org/teavm/classlib/support/GCSupport.java b/tests/src/test/java/org/teavm/classlib/support/GCSupport.java new file mode 100644 index 000000000..591996020 --- /dev/null +++ b/tests/src/test/java/org/teavm/classlib/support/GCSupport.java @@ -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>(); + 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 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; + } +} diff --git a/tools/browser-runner/src/main/java/org/teavm/browserrunner/BrowserRunner.java b/tools/browser-runner/src/main/java/org/teavm/browserrunner/BrowserRunner.java index 716bbceb2..0cc17ccb4 100644 --- a/tools/browser-runner/src/main/java/org/teavm/browserrunner/BrowserRunner.java +++ b/tools/browser-runner/src/main/java/org/teavm/browserrunner/BrowserRunner.java @@ -420,6 +420,7 @@ public class BrowserRunner { "--disable-gpu", "--remote-debugging-port=9222", "--no-first-run", + "--js-flags=--expose-gc", "--user-data-dir=" + profile )); });