Skip to content

Commit 2010a9c

Browse files
committed
Wasm: Implement ju.ArrayList without js.Array
The original implementation of ju.ArrayList uses js.Array as its internal data structure. When compiling to Wasm, operations on ju.ArrayList require JS interop calls to access the underlying js.Array, which causes a slow performance. This commit introduces an implementation of ju.ArrayList for the Wasm backend. This version uses Scala's Array instead of js.Array for better performance.
1 parent e26653c commit 2010a9c

File tree

3 files changed

+162
-22
lines changed

3 files changed

+162
-22
lines changed

javalib/src/main/scala/java/util/ArrayList.scala

+124-21
Original file line numberDiff line numberDiff line change
@@ -14,75 +14,159 @@ package java.util
1414

1515
import java.lang.Cloneable
1616
import java.lang.Utils._
17+
import java.util.ScalaOps._
1718

1819
import scala.scalajs._
20+
import scala.scalajs.LinkingInfo.isWebAssembly
1921

20-
class ArrayList[E] private (private[ArrayList] val inner: js.Array[E])
22+
class ArrayList[E] private (innerInit: AnyRef, private var _size: Int)
2123
extends AbstractList[E] with RandomAccess with Cloneable with Serializable {
2224
self =>
2325

26+
/* This class has two different implementations for handling the
27+
* internal data storage, depending on whether we are on Wasm or JS.
28+
* On JS, we utilize `js.Array`. On Wasm, for performance reasons,
29+
* we avoid JS interop and use a scala.Array.
30+
* The `_size` field (unused in JS) keeps track of the effective size
31+
* of the underlying Array for the Wasm implementation.
32+
*/
33+
34+
private val innerJS: js.Array[E] =
35+
if (isWebAssembly) null
36+
else innerInit.asInstanceOf[js.Array[E]]
37+
38+
private var innerWasm: Array[AnyRef] =
39+
if (!isWebAssembly) null
40+
else innerInit.asInstanceOf[Array[AnyRef]]
41+
2442
def this(initialCapacity: Int) = {
25-
this(new js.Array[E])
26-
if (initialCapacity < 0)
27-
throw new IllegalArgumentException
43+
this(
44+
{
45+
if (initialCapacity < 0)
46+
throw new IllegalArgumentException
47+
if (isWebAssembly) new Array[Any](initialCapacity)
48+
else new js.Array[E]
49+
},
50+
0
51+
)
2852
}
2953

30-
def this() =
31-
this(new js.Array[E])
54+
def this() = this(16)
3255

3356
def this(c: Collection[_ <: E]) = {
34-
this()
57+
this(c.size())
3558
addAll(c)
3659
}
3760

3861
def trimToSize(): Unit = {
39-
// We ignore this as js.Array doesn't support explicit pre-allocation
62+
if (isWebAssembly)
63+
resizeTo(size())
64+
// We ignore this in JS as js.Array doesn't support explicit pre-allocation
4065
}
4166

4267
def ensureCapacity(minCapacity: Int): Unit = {
43-
// We ignore this as js.Array doesn't support explicit pre-allocation
68+
if (isWebAssembly) {
69+
if (innerWasm.length < minCapacity) {
70+
if (minCapacity > 1073741824) { // 2^30
71+
resizeTo(minCapacity)
72+
} else {
73+
var powerOf2: Int = 1
74+
while (powerOf2 < minCapacity)
75+
powerOf2 *= 2
76+
resizeTo(powerOf2)
77+
}
78+
}
79+
}
80+
// We ignore this in JS as js.Array doesn't support explicit pre-allocation
4481
}
4582

4683
def size(): Int =
47-
inner.length
48-
49-
override def clone(): AnyRef =
50-
new ArrayList(inner.jsSlice(0))
84+
if (isWebAssembly) _size
85+
else innerJS.length
86+
87+
override def clone(): AnyRef = {
88+
if (isWebAssembly)
89+
new ArrayList(innerWasm.clone(), size())
90+
else
91+
new ArrayList(innerJS.jsSlice(0), 0)
92+
}
5193

5294
def get(index: Int): E = {
5395
checkIndexInBounds(index)
54-
inner(index)
96+
if (isWebAssembly)
97+
innerWasm(index).asInstanceOf[E]
98+
else
99+
innerJS(index)
55100
}
56101

57102
override def set(index: Int, element: E): E = {
58103
val e = get(index)
59-
inner(index) = element
104+
if (isWebAssembly)
105+
innerWasm(index) = element.asInstanceOf[AnyRef]
106+
else
107+
innerJS(index) = element
60108
e
61109
}
62110

63111
override def add(e: E): Boolean = {
64-
inner.push(e)
112+
if (isWebAssembly) {
113+
if (size() >= innerWasm.length)
114+
expand()
115+
innerWasm(size()) = e.asInstanceOf[AnyRef]
116+
_size += 1
117+
} else {
118+
innerJS.push(e)
119+
}
65120
true
66121
}
67122

68123
override def add(index: Int, element: E): Unit = {
69124
checkIndexOnBounds(index)
70-
inner.splice(index, 0, element)
125+
if (isWebAssembly) {
126+
if (size() >= innerWasm.length)
127+
expand()
128+
System.arraycopy(innerWasm, index, innerWasm, index + 1, size() - index)
129+
innerWasm(index) = element.asInstanceOf[AnyRef]
130+
_size += 1
131+
} else {
132+
innerJS.splice(index, 0, element)
133+
}
71134
}
72135

73136
override def remove(index: Int): E = {
74137
checkIndexInBounds(index)
75-
arrayRemoveAndGet(inner, index)
138+
if (isWebAssembly) {
139+
val removed = innerWasm(index).asInstanceOf[E]
140+
System.arraycopy(innerWasm, index + 1, innerWasm, index, size() - index - 1)
141+
innerWasm(size - 1) = null
142+
_size -= 1
143+
removed
144+
} else {
145+
arrayRemoveAndGet(innerJS, index)
146+
}
76147
}
77148

78149
override def clear(): Unit =
79-
inner.length = 0
150+
if (isWebAssembly) {
151+
Arrays.fill(innerWasm, null) // free references for GC
152+
_size = 0
153+
} else {
154+
innerJS.length = 0
155+
}
80156

81157
override def addAll(index: Int, c: Collection[_ <: E]): Boolean = {
82158
c match {
83159
case other: ArrayList[_] =>
84160
checkIndexOnBounds(index)
85-
inner.splice(index, 0, other.inner.toSeq: _*)
161+
if (isWebAssembly) {
162+
if (size() + other.size() >= innerWasm.length)
163+
resizeTo(size() + other.size())
164+
System.arraycopy(innerWasm, index, innerWasm, index + other.size(), size() - index)
165+
System.arraycopy(other.innerWasm, 0, innerWasm, index, other.size())
166+
_size += c.size()
167+
} else {
168+
innerJS.splice(index, 0, other.innerJS.toSeq: _*)
169+
}
86170
other.size() > 0
87171
case _ => super.addAll(index, c)
88172
}
@@ -91,6 +175,25 @@ class ArrayList[E] private (private[ArrayList] val inner: js.Array[E])
91175
override protected def removeRange(fromIndex: Int, toIndex: Int): Unit = {
92176
if (fromIndex < 0 || toIndex > size() || toIndex < fromIndex)
93177
throw new IndexOutOfBoundsException()
94-
inner.splice(fromIndex, toIndex - fromIndex)
178+
if (isWebAssembly) {
179+
if (fromIndex != toIndex) {
180+
System.arraycopy(innerWasm, toIndex, innerWasm, fromIndex, size() - toIndex)
181+
val newSize = size() - toIndex + fromIndex
182+
Arrays.fill(innerWasm, newSize, size(), null) // free references for GC
183+
_size = newSize
184+
}
185+
} else {
186+
innerJS.splice(fromIndex, toIndex - fromIndex)
187+
}
188+
}
189+
190+
// Wasm only
191+
private def expand(): Unit = {
192+
resizeTo(Math.max(innerWasm.length * 2, 16))
193+
}
194+
195+
// Wasm only
196+
private def resizeTo(newCapacity: Int): Unit = {
197+
innerWasm = Arrays.copyOf(innerWasm, newCapacity)
95198
}
96199
}

test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/util/ArrayListTest.scala

+22-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import scala.reflect.ClassTag
2323

2424
class ArrayListTest extends AbstractListTest {
2525

26-
override def factory: AbstractListFactory = new ArrayListFactory
26+
override def factory: ArrayListFactory = new ArrayListFactory
2727

2828
@Test def ensureCapacity(): Unit = {
2929
// note that these methods become no ops in js
@@ -46,6 +46,27 @@ class ArrayListTest extends AbstractListTest {
4646
al.addAll(al.size + 1, TrivialImmutableCollection("foo")))
4747
}
4848

49+
@Test def constructorInitialCapacity(): Unit = {
50+
val al1 = new ju.ArrayList(0)
51+
assertTrue(al1.size() == 0)
52+
assertTrue(al1.isEmpty())
53+
54+
val al2 = new ju.ArrayList(2)
55+
assertTrue(al2.size() == 0)
56+
assertTrue(al2.isEmpty())
57+
58+
assertThrows(classOf[IllegalArgumentException], new ju.ArrayList(-1))
59+
}
60+
61+
@Test def testClone(): Unit = {
62+
val al1 = factory.fromElements[Int](1, 2)
63+
val al2 = al1.clone().asInstanceOf[ju.ArrayList[Int]]
64+
al1.add(100)
65+
al2.add(200)
66+
assertTrue(Array[Int](1, 2, 100).sameElements(al1.toArray()))
67+
assertTrue(Array[Int](1, 2, 200).sameElements(al2.toArray()))
68+
}
69+
4970
@Test def removeRangeFromIdenticalIndices(): Unit = {
5071
val al = new ArrayListRangeRemovable[Int](
5172
TrivialImmutableCollection(-175, 24, 7, 44))

test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/util/CollectionTest.scala

+16
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ import java.{util => ju, lang => jl}
1616

1717
import org.junit.Test
1818
import org.junit.Assert._
19+
import org.junit.Assume._
1920

2021
import org.scalajs.testsuite.javalib.lang.IterableFactory
2122
import org.scalajs.testsuite.javalib.lang.IterableTest
2223
import org.scalajs.testsuite.utils.AssertThrows.{assertThrows, _}
24+
import org.scalajs.testsuite.utils.Platform
2325
import scala.reflect.ClassTag
2426

2527
import Utils._
@@ -117,6 +119,16 @@ trait CollectionTest extends IterableTest {
117119
assertFalse(coll.contains(TestObj(200)))
118120
}
119121

122+
@Test def isEmpty(): Unit = {
123+
val coll = factory.empty[Int]
124+
assertTrue(coll.size() == 0)
125+
assertTrue(coll.isEmpty())
126+
127+
val nonEmpty = factory.fromElements[Int](1)
128+
assertTrue(nonEmpty.size() == 1)
129+
assertFalse(nonEmpty.isEmpty())
130+
}
131+
120132
@Test def removeString(): Unit = {
121133
val coll = factory.empty[String]
122134

@@ -320,6 +332,10 @@ trait CollectionTest extends IterableTest {
320332
expected.contains(result))
321333
}
322334

335+
@Test def constructorNullThrowsNullPointerException(): Unit = {
336+
assumeTrue("assumed compliant NPEs", Platform.hasCompliantNullPointers)
337+
assertThrows(classOf[NullPointerException], new ju.ArrayList(null))
338+
}
323339
}
324340

325341
trait CollectionFactory extends IterableFactory {

0 commit comments

Comments
 (0)