-
Notifications
You must be signed in to change notification settings - Fork 396
Wasm: Implement ju.ArrayList without js.Array for Wasm backend #5162
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
a1da481
to
b511ec4
Compare
val newArr = new Array[Any](newCapacity) | ||
System.arraycopy(innerWasm, 0, newArr, 0, size()) | ||
innerWasm = newArr |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't use Arrays.copyOf
since innerWasm
needs to be casted to Array[AnyRef]
for filling it with null
, which fails if E was AnyVal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. This looks good overall.
I mostly have coding style comments, as well as comments related to using an Array[AnyRef]
.
@Test def isEmpty(): Unit = { | ||
val coll = factory.empty[Int] | ||
assertTrue(coll.size() == 0) | ||
assertTrue(coll.isEmpty()) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add another case with a non-empty collection (created with fromElements
) to test the false
case.
if (fromIndex < 0 || toIndex > size() || toIndex < fromIndex) { | ||
throw new IndexOutOfBoundsException() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider switching the commits. In the first commit, add the new tests, and fix this missing IndexOutOfBoundsException
. In the second commit, introduce the Wasm rewriting. This will remove the need for the churn with the platform-dependent assumption.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure! Switched the commit order, and added bounds check for addAll to the first commit as well :)
private var innerWasm = | ||
if (!isWebAssembly) null | ||
else innerInit.asInstanceOf[Array[Any]] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest creating this array as an Array[AnyRef]
. They erase to the same thing, but Array[AnyRef]
will allow to use the methods of Arrays
. It's common practice even inside the Scala standard library to use an Array[AnyRef]
when we need a performant "array of anything".
Note that it is always valid to cast to AnyRef
, even for values that "are AnyVal
s". AnyVal
is a Scala compiler fiction. It erases to AnyRef
as well. Casting to AnyRef
is a no-op in the IR/bytecode.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that it is always valid to cast to AnyRef, even for values that "are AnyVals". AnyVal is a Scala compiler fiction. It erases to AnyRef as well. Casting to AnyRef is a no-op in the IR/bytecode.
Interesting, I'll play around with it, but for now we cast it to Array[AnyRef]
and can use the Arrays
functions. Thanks!
if (isWebAssembly) | ||
new Array[Any](Math.max(initialCapacity, 0)) | ||
else new js.Array[E], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (isWebAssembly) | |
new Array[Any](Math.max(initialCapacity, 0)) | |
else new js.Array[E], | |
if (isWebAssembly) new Array[Any](Math.max(initialCapacity, 0)) | |
else new js.Array[E], |
To avoid the Math.max
, though, we can move the initialCapacity < 0
check before the super call, like this:
def this(initialCapacity: Int) = {
this(
{
if (initialCapacity < 0)
throw new IllegalArgumentException
if (isWebAssembly) new Array[Any](initialCapacity)
else new js.Array[E],
},
0
}
addAll(c) | ||
} | ||
|
||
def trimToSize(): Unit = { | ||
// We ignore this as js.Array doesn't support explicit pre-allocation | ||
if (isWebAssembly) resizeTo(size()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Coding style: Always use a separate line for side-effecting branches of control structures (see here)
} | ||
|
||
override def clear(): Unit = | ||
inner.length = 0 | ||
if (isWebAssembly) { | ||
removeRange(0, size()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This never needs to check bounds nor move elements. Consider using a direct Arrays.fill
:
removeRange(0, size()) | |
Arrays.fill(innerWasm, null) // free references for GC |
|
||
override def addAll(index: Int, c: Collection[_ <: E]): Boolean = { | ||
c match { | ||
case other: ArrayList[_] => | ||
inner.splice(index, 0, other.inner.toSeq: _*) | ||
if (isWebAssembly) { | ||
checkIndexOnBounds(index) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like another bounds check that was missing from the JS implementation, and that should be applied to both cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, right, added the bound check in the first commit (and check in both JS and Wasm in second) 👍
for (i <- (size() - toIndex + fromIndex) until size()) { | ||
innerWasm(i) = null | ||
} | ||
_size -= (toIndex - fromIndex) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider computing val newSize = _size - (toIndex - fromIndex)
first, to use it also as the start index for the null-filling. Currently this code performs that computation twice, but in slightly different ways, which is confusing.
Also, with an Array[AnyRef]
the loop is an Arrays.fill
.
|
||
// Wasm only | ||
private def expand(): Unit = { | ||
resizeTo(Math.max(innerWasm.length * 2, 1)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if it would be better to use 16 instead of 1 as lower bound here.
If we need to resize once, we probably need to resize more later, so we should directly go with the default 16 that we use when no initial capacity is provided.
assertTrue(Array[Int](1, 2, 100) sameElements al1.toArray()) | ||
assertTrue(Array[Int](1, 2, 200) sameElements al2.toArray()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Coding style: We don't use the infix syntax for alphanumeric method calls anymore (except for eq
, ne
, to
and until
):
assertTrue(Array[Int](1, 2, 100) sameElements al1.toArray()) | |
assertTrue(Array[Int](1, 2, 200) sameElements al2.toArray()) | |
assertTrue(Array[Int](1, 2, 100).sameElements(al1.toArray())) | |
assertTrue(Array[Int](1, 2, 200).sameElements(al2.toArray())) |
Same several times below.
a7d756d
to
443c741
Compare
Thanks! I think addressed all the feedbacks. |
443c741
to
7d87768
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional drive-by suggestions
if (isWebAssembly) new Array[AnyRef](16) | ||
else new js.Array[E], | ||
0 | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just this(16)
? Let the other ctor handle wasm v.s. JS?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, nice catch, thanks!
if (isWebAssembly) new Array[AnyRef](c.size()) | ||
else new js.Array[E], | ||
0 | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe also here this(c.size())
?
// We ignore this as js.Array doesn't support explicit pre-allocation | ||
if (isWebAssembly) { | ||
if (innerWasm.length < minCapacity) | ||
resizeTo(minCapacity) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we round up to the next power of 2 here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2010a9c
to
7a0e0c6
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few last details, but otherwise looks good to me.
test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/util/ArrayListTest.scala
Outdated
Show resolved
Hide resolved
test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/util/CollectionTest.scala
Outdated
Show resolved
Hide resolved
7a0e0c6
to
99cd41e
Compare
removeRange should throws an `IndexOutOfBoundsException` if fromIndex or toIndex is out of range. addAll should also throw when the index is not within bounds.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I took the liberty to move the addAll
test to the right commit.
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.
The second commit fixes JS implementation of
removeRange
to throw anIndexOutOfBoundsException
if fromIndex or toIndex is out of range.Upstreamed from scala-wasm#39