From 808a1308403c7c52ffca1242fd04786301c0f961 Mon Sep 17 00:00:00 2001 From: Rikito Taniguchi Date: Sat, 17 May 2025 13:12:39 +0900 Subject: [PATCH 1/3] Add java.lang.Utils.roundUpToPowerOfTwo utility function The use-case of this function is for ensuring the size of internal buffers (e.g. internal Array of `ju.ArrayList`). --- javalib/src/main/scala/java/lang/Utils.scala | 5 +++++ javalib/src/main/scala/java/util/ArrayList.scala | 8 ++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/javalib/src/main/scala/java/lang/Utils.scala b/javalib/src/main/scala/java/lang/Utils.scala index 94d323e026..a9757ad301 100644 --- a/javalib/src/main/scala/java/lang/Utils.scala +++ b/javalib/src/main/scala/java/lang/Utils.scala @@ -192,4 +192,9 @@ private[java] object Utils { import js.DynamicImplicits.number2dynamic (x >>> 0).asInstanceOf[scala.Double] } + + /** Round up to a power of 2; if overflow, returns the given number. */ + @inline def roundUpToPowerOfTwo(i: Int): Int = + if (i > (1 << 30)) i + else ((1 << 31) >>> (Integer.numberOfLeadingZeros(i - 1)) - 1) } diff --git a/javalib/src/main/scala/java/util/ArrayList.scala b/javalib/src/main/scala/java/util/ArrayList.scala index 1c67de682b..3b4e8f5f97 100644 --- a/javalib/src/main/scala/java/util/ArrayList.scala +++ b/javalib/src/main/scala/java/util/ArrayList.scala @@ -66,12 +66,8 @@ class ArrayList[E] private (innerInit: AnyRef, private var _size: Int) def ensureCapacity(minCapacity: Int): Unit = { if (isWebAssembly) { - if (innerWasm.length < minCapacity) { - if (minCapacity > (1 << 30)) - resizeTo(minCapacity) - else - resizeTo(((1 << 31) >>> (Integer.numberOfLeadingZeros(minCapacity - 1)) - 1)) - } + if (innerWasm.length < minCapacity) + resizeTo(roundUpToPowerOfTwo(minCapacity)) } // We ignore this in JS as js.Array doesn't support explicit pre-allocation } From 3783b54ff8208445dd13dc8dbd5de2888df0be77 Mon Sep 17 00:00:00 2001 From: Rikito Taniguchi Date: Sat, 17 May 2025 13:19:38 +0900 Subject: [PATCH 2/3] Wasm: Implement PriorityQueue without js.Array in Wasm backend Current js.Array-based PriorityQueue implementation on Wasm requires JS-interop for every operation, and JS-interop is very slow. We use a `linkTimeIf` to select a `scala.Array`-based implementation of internal data structure for PriorityQueue, based on wether it is on JS or WebAssembly. --- .../main/scala/java/util/PriorityQueue.scala | 221 ++++++++++++++---- 1 file changed, 172 insertions(+), 49 deletions(-) diff --git a/javalib/src/main/scala/java/util/PriorityQueue.scala b/javalib/src/main/scala/java/util/PriorityQueue.scala index 9fc348472f..427ba77438 100644 --- a/javalib/src/main/scala/java/util/PriorityQueue.scala +++ b/javalib/src/main/scala/java/util/PriorityQueue.scala @@ -12,31 +12,46 @@ package java.util +import java.lang.Utils.roundUpToPowerOfTwo + import scala.annotation.tailrec import scala.scalajs.js +import scala.scalajs.LinkingInfo class PriorityQueue[E] private ( - private val comp: Comparator[_ >: E], internal: Boolean) + private val comp: Comparator[_ >: E], internal: Boolean, initialCapacity: Int) extends AbstractQueue[E] with Serializable { def this() = - this(NaturalComparator, internal = true) + this(NaturalComparator, internal = true, initialCapacity = 16) def this(initialCapacity: Int) = { - this() - if (initialCapacity < 1) - throw new IllegalArgumentException() + this( + NaturalComparator, + internal = true, + { + if (initialCapacity < 1) + throw new IllegalArgumentException + initialCapacity + } + ) } def this(comparator: Comparator[_ >: E]) = { - this(NaturalComparator.select(comparator), internal = true) + this(NaturalComparator.select(comparator), internal = true, initialCapacity = 16) } def this(initialCapacity: Int, comparator: Comparator[_ >: E]) = { - this(comparator) - if (initialCapacity < 1) - throw new IllegalArgumentException() + this( + NaturalComparator.select(comparator), + internal = true, + { + if (initialCapacity < 1) + throw new IllegalArgumentException() + initialCapacity + } + ) } def this(c: Collection[_ <: E]) = { @@ -47,47 +62,74 @@ class PriorityQueue[E] private ( NaturalComparator.select(c.comparator().asInstanceOf[Comparator[_ >: E]]) case _ => NaturalComparator - }, internal = true) + }, internal = true, roundUpToPowerOfTwo(c.size())) addAll(c) } def this(c: PriorityQueue[_ <: E]) = { - this(c.comp.asInstanceOf[Comparator[_ >: E]], internal = true) + this(c.comp.asInstanceOf[Comparator[_ >: E]], internal = true, roundUpToPowerOfTwo(c.size())) addAll(c) } def this(sortedSet: SortedSet[_ <: E]) = { this(NaturalComparator.select( sortedSet.comparator().asInstanceOf[Comparator[_ >: E]]), - internal = true) + internal = true, + roundUpToPowerOfTwo(sortedSet.size())) addAll(sortedSet) } + /* Get the best available implementation of inner array for the given platform. + * + * Use Array[AnyRef] in WebAssembly to avoid JS-interop. In JS, use js.Array. + * It is resizable by nature, so manual resizing is not needed. + * + * `linkTimeIf` is needed here to ensure the optimizer knows + * there is only one implementation of `InnerArrayImpl`, and de-virtualize/inline + * the function calls. + */ + + private val innerImpl: InnerArrayImpl = + LinkingInfo.linkTimeIf[InnerArrayImpl](LinkingInfo.isWebAssembly) { + InnerArrayImpl.JArrayImpl + } { + InnerArrayImpl.JSArrayImpl + } + + private var inner: innerImpl.Repr = innerImpl.make(initialCapacity) + + // Wasm only // The index 0 is not used; the root is at index 1. // This is standard practice in binary heaps, to simplify arithmetics. - private[this] val inner = js.Array[E](null.asInstanceOf[E]) + private var _size = 1 override def add(e: E): Boolean = { if (e == null) throw new NullPointerException() - inner.push(e) - fixUp(inner.length - 1) + + if (LinkingInfo.isWebAssembly) { + val minCapacity = innerImpl.length(inner) + 1 + if (innerImpl.capacity(inner) < minCapacity) + inner = innerImpl.resized(inner, minCapacity) + } + innerImpl.push(inner, e) + fixUp(innerImpl.length(inner) - 1) true } def offer(e: E): Boolean = add(e) def peek(): E = - if (inner.length > 1) inner(1) + if (innerImpl.length(inner) > 1) innerImpl.get(inner, 1) else null.asInstanceOf[E] override def remove(o: Any): Boolean = { if (o == null) { false } else { - val len = inner.length + val len = innerImpl.length(inner) var i = 1 - while (i != len && !o.equals(inner(i))) { + while (i != len && !o.equals(innerImpl.get(inner, i))) { i += 1 } @@ -101,9 +143,9 @@ class PriorityQueue[E] private ( } private def removeExact(o: Any): Unit = { - val len = inner.length + val len = innerImpl.length(inner) var i = 1 - while (i != len && (o.asInstanceOf[AnyRef] ne inner(i).asInstanceOf[AnyRef])) { + while (i != len && (o.asInstanceOf[AnyRef] ne innerImpl.get(inner, i).asInstanceOf[AnyRef])) { i += 1 } if (i == len) @@ -112,23 +154,25 @@ class PriorityQueue[E] private ( } private def removeAt(i: Int): Unit = { - val newLength = inner.length - 1 + val newLength = innerImpl.length(inner) - 1 if (i == newLength) { - inner.length = newLength + innerImpl.setLength(inner, newLength) } else { - inner(i) = inner(newLength) - inner.length = newLength + innerImpl.set(inner, i, innerImpl.get(inner, newLength)) + innerImpl.setLength(inner, newLength) fixUpOrDown(i) } + if (LinkingInfo.isWebAssembly) + innerImpl.set(inner, innerImpl.length(inner), null.asInstanceOf[E]) // free reference for GC } override def contains(o: Any): Boolean = { if (o == null) { false } else { - val len = inner.length + val len = innerImpl.length(inner) var i = 1 - while (i != len && !o.equals(inner(i))) { + while (i != len && !o.equals(innerImpl.get(inner, i))) { i += 1 } i != len @@ -137,16 +181,20 @@ class PriorityQueue[E] private ( def iterator(): Iterator[E] = { new Iterator[E] { - private[this] var inner: js.Array[E] = PriorityQueue.this.inner + private[this] var inner: innerImpl.Repr = PriorityQueue.this.inner + // Wasm only + private[this] var innerIterSize: Int = innerImpl.length(PriorityQueue.this.inner) private[this] var nextIdx: Int = 1 private[this] var last: E = _ // null - def hasNext(): Boolean = nextIdx < inner.length + def hasNext(): Boolean = + if (LinkingInfo.isWebAssembly) nextIdx < innerIterSize + else nextIdx < innerImpl.length(inner) def next(): E = { if (!hasNext()) throw new NoSuchElementException("empty iterator") - last = inner(nextIdx) + last = innerImpl.get(inner, nextIdx) nextIdx += 1 last } @@ -173,7 +221,9 @@ class PriorityQueue[E] private ( if (last == null) throw new IllegalStateException() if (inner eq PriorityQueue.this.inner) { - inner = inner.jsSlice(nextIdx) + if (LinkingInfo.isWebAssembly) + innerIterSize = innerImpl.length(inner) - nextIdx + inner = innerImpl.copyFrom(inner, nextIdx) nextIdx = 0 } removeExact(last) @@ -182,19 +232,21 @@ class PriorityQueue[E] private ( } } - def size(): Int = inner.length - 1 + def size(): Int = innerImpl.length(inner) - 1 override def clear(): Unit = - inner.length = 1 + innerImpl.clear(inner) def poll(): E = { val inner = this.inner // local copy - if (inner.length > 1) { - val newSize = inner.length - 1 - val result = inner(1) - inner(1) = inner(newSize) - inner.length = newSize + if (innerImpl.length(inner) > 1) { + val newSize = innerImpl.length(inner) - 1 + val result = innerImpl.get(inner, 1) + innerImpl.set(inner, 1, innerImpl.get(inner, newSize)) + innerImpl.setLength(inner, newSize) fixDown(1) + if (LinkingInfo.isWebAssembly) + innerImpl.set(inner, newSize, null.asInstanceOf[E]) // free reference for GC result } else { null.asInstanceOf[E] @@ -212,7 +264,7 @@ class PriorityQueue[E] private ( */ private[this] def fixUpOrDown(m: Int): Unit = { val inner = this.inner // local copy - if (m > 1 && comp.compare(inner(m >> 1), inner(m)) > 0) + if (m > 1 && comp.compare(innerImpl.get(inner, m >> 1), innerImpl.get(inner, m)) > 0) fixUp(m) else fixDown(m) @@ -227,18 +279,18 @@ class PriorityQueue[E] private ( /* At each step, even though `m` changes, the element moves with it, and * hence inner(m) is always the same initial `innerAtM`. */ - val innerAtM = inner(m) + val innerAtM = innerImpl.get(inner, m) @inline @tailrec def loop(m: Int): Unit = { if (m > 1) { val parent = m >> 1 - val innerAtParent = inner(parent) + val innerAtParent = innerImpl.get(inner, parent) if (comp.compare(innerAtParent, innerAtM) > 0) { - inner(parent) = innerAtM - inner(m) = innerAtParent - loop(parent) + innerImpl.set(inner, parent, innerAtM) + innerImpl.set(inner, m, innerAtParent) } + loop(parent) } } @@ -250,22 +302,22 @@ class PriorityQueue[E] private ( */ private[this] def fixDown(m: Int): Unit = { val inner = this.inner // local copy - val size = inner.length - 1 + val size = innerImpl.length(inner) - 1 /* At each step, even though `m` changes, the element moves with it, and * hence inner(m) is always the same initial `innerAtM`. */ - val innerAtM = inner(m) + val innerAtM = innerImpl.get(inner, m) @inline @tailrec def loop(m: Int): Unit = { var j = 2 * m // left child of `m` if (j <= size) { - var innerAtJ = inner(j) + var innerAtJ = innerImpl.get(inner, j) // if the left child is greater than the right child, switch to the right child if (j < size) { - val innerAtJPlus1 = inner(j + 1) + val innerAtJPlus1 = innerImpl.get(inner, j + 1) if (comp.compare(innerAtJ, innerAtJPlus1) > 0) { j += 1 innerAtJ = innerAtJPlus1 @@ -274,8 +326,8 @@ class PriorityQueue[E] private ( // if the node `m` is greater than the selected child, swap and recurse if (comp.compare(innerAtM, innerAtJ) > 0) { - inner(m) = innerAtJ - inner(j) = innerAtM + innerImpl.set(inner, m, innerAtJ) + innerImpl.set(inner, j, innerAtM) loop(j) } } @@ -283,4 +335,75 @@ class PriorityQueue[E] private ( loop(m) } + + private sealed abstract class InnerArrayImpl { + type Repr <: AnyRef + + def make(initialCapacity: Int): Repr + def length(v: Repr): Int + /** Set the length of innerArray. + * + * In WebAssembly, freeing the reference for GC is needed + * when we shrink the inner array. + */ + def setLength(v: Repr, newLength: Int): Unit + def get(v: Repr, index: Int): E + def set(v: Repr, index: Int, e: E): Unit + def push(v: Repr, e: E): Unit + /** Wasm only. */ + def resized(v: Repr, minCapacity: Int): Repr + /** Wasm only. */ + def capacity(v: Repr): Int + def copyFrom(v: Repr, from: Int): Repr + def clear(v: Repr): Unit + } + + private object InnerArrayImpl { + object JSArrayImpl extends InnerArrayImpl { + type Repr = js.Array[E] + + // The index 0 is not used; the root is at index 1. + // This is standard practice in binary heaps, to simplify arithmetics. + @inline def make(_initialCapacity: Int): Repr = js.Array[E](null.asInstanceOf[E]) + @inline def length(v: Repr): Int = v.length + @inline def setLength(v: Repr, newLength: Int): Unit = + v.length = newLength + @inline def get(v: Repr, index: Int): E = v(index) + @inline def set(v: Repr, index: Int, e: E): Unit = + v(index) = e + @inline def push(v: Repr, e: E): Unit = + v.push(e) + @inline def resized(v: Repr, minCapacity: Int): Repr = v // no used + @inline def capacity(v: Repr): Int = 0 // no used + @inline def copyFrom(v: Repr, from: Int): Repr = + v.jsSlice(from) + @inline def clear(v: Repr): Unit = + v.length = 1 + } + + object JArrayImpl extends InnerArrayImpl { + type Repr = Array[AnyRef] + + @inline def make(initialCapacity: Int): Repr = new Array[AnyRef](initialCapacity) + @inline def length(v: Repr): Int = _size + @inline def setLength(v: Repr, newLength: Int): Unit = + _size = newLength + @inline def get(v: Repr, index: Int): E = v(index).asInstanceOf[E] + @inline def set(v: Repr, index: Int, e: E): Unit = + v(index) = e.asInstanceOf[AnyRef] + @inline def push(v: Repr, e: E): Unit = { + v(_size) = e.asInstanceOf[AnyRef] + _size += 1 + } + @inline def resized(v: Repr, minCapacity: Int): Repr = + Arrays.copyOf(v, roundUpToPowerOfTwo(minCapacity)) + @inline def capacity(v: Repr): Int = v.length + @inline def copyFrom(v: Repr, from: Int): Repr = + Arrays.copyOfRange(v, from, _size) + @inline def clear(v: Repr): Unit = { + Arrays.fill(v, null) + _size = 1 + } + } + } } From 144bca0fedee8f052f27cd1fffa069548cac9108 Mon Sep 17 00:00:00 2001 From: Rikito Taniguchi Date: Wed, 11 Jun 2025 17:38:42 +0900 Subject: [PATCH 3/3] Move innerArrayImpl to companion object Also, store the size of array in the first element of Array --- .../main/scala/java/util/PriorityQueue.scala | 161 ++++++++++-------- 1 file changed, 88 insertions(+), 73 deletions(-) diff --git a/javalib/src/main/scala/java/util/PriorityQueue.scala b/javalib/src/main/scala/java/util/PriorityQueue.scala index 427ba77438..94145067ed 100644 --- a/javalib/src/main/scala/java/util/PriorityQueue.scala +++ b/javalib/src/main/scala/java/util/PriorityQueue.scala @@ -12,17 +12,18 @@ package java.util -import java.lang.Utils.roundUpToPowerOfTwo - import scala.annotation.tailrec -import scala.scalajs.js +import java.lang.Utils.roundUpToPowerOfTwo + import scala.scalajs.LinkingInfo class PriorityQueue[E] private ( private val comp: Comparator[_ >: E], internal: Boolean, initialCapacity: Int) extends AbstractQueue[E] with Serializable { + import PriorityQueue._ + def this() = this(NaturalComparator, internal = true, initialCapacity = 16) @@ -33,7 +34,7 @@ class PriorityQueue[E] private ( { if (initialCapacity < 1) throw new IllegalArgumentException - initialCapacity + initialCapacity + 1 // index 0 is unused } ) } @@ -49,7 +50,7 @@ class PriorityQueue[E] private ( { if (initialCapacity < 1) throw new IllegalArgumentException() - initialCapacity + initialCapacity + 1 // index 0 is unused } ) } @@ -62,12 +63,13 @@ class PriorityQueue[E] private ( NaturalComparator.select(c.comparator().asInstanceOf[Comparator[_ >: E]]) case _ => NaturalComparator - }, internal = true, roundUpToPowerOfTwo(c.size())) + }, internal = true, roundUpToPowerOfTwo(c.size() + 1)) // index 0 is unused addAll(c) } def this(c: PriorityQueue[_ <: E]) = { - this(c.comp.asInstanceOf[Comparator[_ >: E]], internal = true, roundUpToPowerOfTwo(c.size())) + this(c.comp.asInstanceOf[Comparator[_ >: E]], internal = true, + roundUpToPowerOfTwo(c.size() + 1)) // index 0 is unused addAll(c) } @@ -75,34 +77,12 @@ class PriorityQueue[E] private ( this(NaturalComparator.select( sortedSet.comparator().asInstanceOf[Comparator[_ >: E]]), internal = true, - roundUpToPowerOfTwo(sortedSet.size())) + roundUpToPowerOfTwo(sortedSet.size() + 1)) // index 0 is unused addAll(sortedSet) } - /* Get the best available implementation of inner array for the given platform. - * - * Use Array[AnyRef] in WebAssembly to avoid JS-interop. In JS, use js.Array. - * It is resizable by nature, so manual resizing is not needed. - * - * `linkTimeIf` is needed here to ensure the optimizer knows - * there is only one implementation of `InnerArrayImpl`, and de-virtualize/inline - * the function calls. - */ - - private val innerImpl: InnerArrayImpl = - LinkingInfo.linkTimeIf[InnerArrayImpl](LinkingInfo.isWebAssembly) { - InnerArrayImpl.JArrayImpl - } { - InnerArrayImpl.JSArrayImpl - } - private var inner: innerImpl.Repr = innerImpl.make(initialCapacity) - // Wasm only - // The index 0 is not used; the root is at index 1. - // This is standard practice in binary heaps, to simplify arithmetics. - private var _size = 1 - override def add(e: E): Boolean = { if (e == null) throw new NullPointerException() @@ -112,7 +92,7 @@ class PriorityQueue[E] private ( if (innerImpl.capacity(inner) < minCapacity) inner = innerImpl.resized(inner, minCapacity) } - innerImpl.push(inner, e) + innerImpl.push(inner, e.asInstanceOf[AnyRef]) fixUp(innerImpl.length(inner) - 1) true } @@ -120,7 +100,7 @@ class PriorityQueue[E] private ( def offer(e: E): Boolean = add(e) def peek(): E = - if (innerImpl.length(inner) > 1) innerImpl.get(inner, 1) + if (innerImpl.length(inner) > 1) innerImpl.get(inner, 1).asInstanceOf[E] else null.asInstanceOf[E] override def remove(o: Any): Boolean = { @@ -163,7 +143,7 @@ class PriorityQueue[E] private ( fixUpOrDown(i) } if (LinkingInfo.isWebAssembly) - innerImpl.set(inner, innerImpl.length(inner), null.asInstanceOf[E]) // free reference for GC + innerImpl.set(inner, innerImpl.length(inner), null) // free reference for GC } override def contains(o: Any): Boolean = { @@ -182,19 +162,15 @@ class PriorityQueue[E] private ( def iterator(): Iterator[E] = { new Iterator[E] { private[this] var inner: innerImpl.Repr = PriorityQueue.this.inner - // Wasm only - private[this] var innerIterSize: Int = innerImpl.length(PriorityQueue.this.inner) private[this] var nextIdx: Int = 1 private[this] var last: E = _ // null - def hasNext(): Boolean = - if (LinkingInfo.isWebAssembly) nextIdx < innerIterSize - else nextIdx < innerImpl.length(inner) + def hasNext(): Boolean = nextIdx < innerImpl.length(inner) def next(): E = { if (!hasNext()) throw new NoSuchElementException("empty iterator") - last = innerImpl.get(inner, nextIdx) + last = innerImpl.get(inner, nextIdx).asInstanceOf[E] nextIdx += 1 last } @@ -221,10 +197,8 @@ class PriorityQueue[E] private ( if (last == null) throw new IllegalStateException() if (inner eq PriorityQueue.this.inner) { - if (LinkingInfo.isWebAssembly) - innerIterSize = innerImpl.length(inner) - nextIdx inner = innerImpl.copyFrom(inner, nextIdx) - nextIdx = 0 + nextIdx = 1 } removeExact(last) last = null.asInstanceOf[E] @@ -246,8 +220,8 @@ class PriorityQueue[E] private ( innerImpl.setLength(inner, newSize) fixDown(1) if (LinkingInfo.isWebAssembly) - innerImpl.set(inner, newSize, null.asInstanceOf[E]) // free reference for GC - result + innerImpl.set(inner, newSize, null) // free reference for GC + result.asInstanceOf[E] } else { null.asInstanceOf[E] } @@ -264,7 +238,8 @@ class PriorityQueue[E] private ( */ private[this] def fixUpOrDown(m: Int): Unit = { val inner = this.inner // local copy - if (m > 1 && comp.compare(innerImpl.get(inner, m >> 1), innerImpl.get(inner, m)) > 0) + if (m > 1 && comp.compare(innerImpl.get(inner, m >> 1).asInstanceOf[E], + innerImpl.get(inner, m).asInstanceOf[E]) > 0) fixUp(m) else fixDown(m) @@ -279,15 +254,15 @@ class PriorityQueue[E] private ( /* At each step, even though `m` changes, the element moves with it, and * hence inner(m) is always the same initial `innerAtM`. */ - val innerAtM = innerImpl.get(inner, m) + val innerAtM = innerImpl.get(inner, m).asInstanceOf[E] @inline @tailrec def loop(m: Int): Unit = { if (m > 1) { val parent = m >> 1 val innerAtParent = innerImpl.get(inner, parent) - if (comp.compare(innerAtParent, innerAtM) > 0) { - innerImpl.set(inner, parent, innerAtM) + if (comp.compare(innerAtParent.asInstanceOf[E], innerAtM) > 0) { + innerImpl.set(inner, parent, innerAtM.asInstanceOf[AnyRef]) innerImpl.set(inner, m, innerAtParent) } loop(parent) @@ -318,14 +293,14 @@ class PriorityQueue[E] private ( // if the left child is greater than the right child, switch to the right child if (j < size) { val innerAtJPlus1 = innerImpl.get(inner, j + 1) - if (comp.compare(innerAtJ, innerAtJPlus1) > 0) { + if (comp.compare(innerAtJ.asInstanceOf[E], innerAtJPlus1.asInstanceOf[E]) > 0) { j += 1 innerAtJ = innerAtJPlus1 } } // if the node `m` is greater than the selected child, swap and recurse - if (comp.compare(innerAtM, innerAtJ) > 0) { + if (comp.compare(innerAtM.asInstanceOf[E], innerAtJ.asInstanceOf[E]) > 0) { innerImpl.set(inner, m, innerAtJ) innerImpl.set(inner, j, innerAtM) loop(j) @@ -336,6 +311,30 @@ class PriorityQueue[E] private ( loop(m) } +} + +object PriorityQueue { + + /* Get the best available implementation of inner array for the given platform. + * + * Use Array[AnyRef] in WebAssembly to avoid JS-interop. In JS, use js.Array. + * It is resizable by nature, so manual resizing is not needed. + * + * `linkTimeIf` is needed here to ensure the optimizer knows + * there is only one implementation of `InnerArrayImpl`, and de-virtualize/inline + * the function calls. + */ + + private val innerImpl: InnerArrayImpl = + LinkingInfo.linkTimeIf[InnerArrayImpl](LinkingInfo.isWebAssembly) { + InnerArrayImpl.JArrayImpl + } { + InnerArrayImpl.JSArrayImpl + } + + + // The index 0 is not used; the root is at index 1. + // This is standard practice in binary heaps, to simplify arithmetics. private sealed abstract class InnerArrayImpl { type Repr <: AnyRef @@ -347,9 +346,9 @@ class PriorityQueue[E] private ( * when we shrink the inner array. */ def setLength(v: Repr, newLength: Int): Unit - def get(v: Repr, index: Int): E - def set(v: Repr, index: Int, e: E): Unit - def push(v: Repr, e: E): Unit + def get(v: Repr, index: Int): AnyRef + def set(v: Repr, index: Int, e: AnyRef): Unit + def push(v: Repr, e: AnyRef): Unit /** Wasm only. */ def resized(v: Repr, minCapacity: Int): Repr /** Wasm only. */ @@ -360,50 +359,66 @@ class PriorityQueue[E] private ( private object InnerArrayImpl { object JSArrayImpl extends InnerArrayImpl { - type Repr = js.Array[E] + import scala.scalajs.js + + type Repr = js.Array[AnyRef] - // The index 0 is not used; the root is at index 1. - // This is standard practice in binary heaps, to simplify arithmetics. - @inline def make(_initialCapacity: Int): Repr = js.Array[E](null.asInstanceOf[E]) + @inline def make(_initialCapacity: Int): Repr = js.Array[AnyRef](null.asInstanceOf[AnyRef]) @inline def length(v: Repr): Int = v.length @inline def setLength(v: Repr, newLength: Int): Unit = v.length = newLength - @inline def get(v: Repr, index: Int): E = v(index) - @inline def set(v: Repr, index: Int, e: E): Unit = + @inline def get(v: Repr, index: Int): AnyRef = v(index) + @inline def set(v: Repr, index: Int, e: AnyRef): Unit = v(index) = e - @inline def push(v: Repr, e: E): Unit = + @inline def push(v: Repr, e: AnyRef): Unit = v.push(e) @inline def resized(v: Repr, minCapacity: Int): Repr = v // no used @inline def capacity(v: Repr): Int = 0 // no used - @inline def copyFrom(v: Repr, from: Int): Repr = - v.jsSlice(from) + @inline def copyFrom(v: Repr, from: Int): Repr = { + val arr = v.jsSlice(from) + arr.unshift(null.asInstanceOf[AnyRef]) + arr + } @inline def clear(v: Repr): Unit = v.length = 1 } + // We store the effective length in the index 0 of the array, + // where is unused slot both in JSArrayImpl and this impl. object JArrayImpl extends InnerArrayImpl { type Repr = Array[AnyRef] - @inline def make(initialCapacity: Int): Repr = new Array[AnyRef](initialCapacity) - @inline def length(v: Repr): Int = _size + @inline def make(initialCapacity: Int): Repr = { + val v = new Array[AnyRef](initialCapacity) + v(0) = 1.asInstanceOf[AnyRef] + v + } + @inline def length(v: Repr): Int = v(0).asInstanceOf[Int] @inline def setLength(v: Repr, newLength: Int): Unit = - _size = newLength - @inline def get(v: Repr, index: Int): E = v(index).asInstanceOf[E] - @inline def set(v: Repr, index: Int, e: E): Unit = + v(0) = newLength.asInstanceOf[AnyRef] + @inline def get(v: Repr, index: Int): AnyRef = v(index) + @inline def set(v: Repr, index: Int, e: AnyRef): Unit = v(index) = e.asInstanceOf[AnyRef] - @inline def push(v: Repr, e: E): Unit = { - v(_size) = e.asInstanceOf[AnyRef] - _size += 1 + @inline def push(v: Repr, e: AnyRef): Unit = { + val l = length(v) + v(l) = e.asInstanceOf[AnyRef] + v(0) = (l + 1).asInstanceOf[AnyRef] } @inline def resized(v: Repr, minCapacity: Int): Repr = Arrays.copyOf(v, roundUpToPowerOfTwo(minCapacity)) @inline def capacity(v: Repr): Int = v.length - @inline def copyFrom(v: Repr, from: Int): Repr = - Arrays.copyOfRange(v, from, _size) + @inline def copyFrom(v: Repr, from: Int): Repr = { + val elemLength = length(v) - from + val newArr = make(roundUpToPowerOfTwo(elemLength + 1)) + newArr(0) = (elemLength + 1).asInstanceOf[AnyRef] + System.arraycopy(v, from, newArr, 1, elemLength) + newArr + } @inline def clear(v: Repr): Unit = { Arrays.fill(v, null) - _size = 1 + v(0) = 1.asInstanceOf[AnyRef] } } } + }