Skip to content

Commit a7d756d

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 a7d756d

File tree

3 files changed

+162
-21
lines changed

3 files changed

+162
-21
lines changed

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

Lines changed: 124 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,75 +14,160 @@ 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 =
35+
if (isWebAssembly) null
36+
else innerInit.asInstanceOf[js.Array[E]]
37+
38+
private var innerWasm =
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

3054
def this() =
31-
this(new js.Array[E])
55+
this(
56+
if (isWebAssembly) new Array[AnyRef](16)
57+
else new js.Array[E],
58+
0
59+
)
3260

3361
def this(c: Collection[_ <: E]) = {
34-
this()
62+
this(
63+
if (isWebAssembly) new Array[AnyRef](c.size())
64+
else new js.Array[E],
65+
0
66+
)
3567
addAll(c)
3668
}
3769

3870
def trimToSize(): Unit = {
39-
// We ignore this as js.Array doesn't support explicit pre-allocation
71+
if (isWebAssembly)
72+
resizeTo(size())
73+
// We ignore this in JS as js.Array doesn't support explicit pre-allocation
4074
}
4175

4276
def ensureCapacity(minCapacity: Int): Unit = {
43-
// We ignore this as js.Array doesn't support explicit pre-allocation
77+
if (isWebAssembly) {
78+
if (innerWasm.length < minCapacity)
79+
resizeTo(minCapacity)
80+
}
81+
// We ignore this in JS as js.Array doesn't support explicit pre-allocation
4482
}
4583

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

5295
def get(index: Int): E = {
5396
checkIndexInBounds(index)
54-
inner(index)
97+
if (isWebAssembly)
98+
innerWasm(index).asInstanceOf[E]
99+
else
100+
innerJS(index)
55101
}
56102

57103
override def set(index: Int, element: E): E = {
58104
val e = get(index)
59-
inner(index) = element
105+
if (isWebAssembly)
106+
innerWasm(index) = element.asInstanceOf[AnyRef]
107+
else
108+
innerJS(index) = element
60109
e
61110
}
62111

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

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

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

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

81158
override def addAll(index: Int, c: Collection[_ <: E]): Boolean = {
82159
c match {
83160
case other: ArrayList[_] =>
84161
checkIndexOnBounds(index)
85-
inner.splice(index, 0, other.inner.toSeq: _*)
162+
if (isWebAssembly) {
163+
if (size() + other.size() >= innerWasm.length)
164+
resizeTo(size() + other.size())
165+
System.arraycopy(innerWasm, index, innerWasm, index + other.size(), size() - index)
166+
System.arraycopy(other.innerWasm, 0, innerWasm, index, other.size())
167+
_size += c.size()
168+
} else {
169+
innerJS.splice(index, 0, other.innerJS.toSeq: _*)
170+
}
86171
other.size() > 0
87172
case _ => super.addAll(index, c)
88173
}
@@ -91,6 +176,25 @@ class ArrayList[E] private (private[ArrayList] val inner: js.Array[E])
91176
override protected def removeRange(fromIndex: Int, toIndex: Int): Unit = {
92177
if (fromIndex < 0 || toIndex > size() || toIndex < fromIndex)
93178
throw new IndexOutOfBoundsException()
94-
inner.splice(fromIndex, toIndex - fromIndex)
179+
if (isWebAssembly) {
180+
if (fromIndex != toIndex) {
181+
System.arraycopy(innerWasm, toIndex, innerWasm, fromIndex, size() - toIndex)
182+
val newSize = size() - toIndex + fromIndex
183+
Arrays.fill(innerWasm, newSize, size(), null) // free references for GC
184+
_size = newSize
185+
}
186+
} else {
187+
innerJS.splice(fromIndex, toIndex - fromIndex)
188+
}
189+
}
190+
191+
// Wasm only
192+
private def expand(): Unit = {
193+
resizeTo(Math.max(innerWasm.length * 2, 16))
194+
}
195+
196+
// Wasm only
197+
private def resizeTo(newCapacity: Int): Unit = {
198+
innerWasm = Arrays.copyOf(innerWasm, newCapacity)
95199
}
96200
}

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

Lines changed: 22 additions & 1 deletion
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

Lines changed: 16 additions & 0 deletions
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(coll.size() == 1)
129+
assertFalse(coll.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)