From 0d5e7b35d85336a6d20c121c1dd07eacdc36491f Mon Sep 17 00:00:00 2001 From: Danielle Voznyy Date: Fri, 1 Nov 2024 18:12:11 -0400 Subject: [PATCH] chore: Update benchmarks to use new query syntax perf: Optimize query iteration performance slightly --- geary-benchmarks/build.gradle.kts | 8 +- .../benchmarks/VelocitySystemBenchmark.kt | 2 +- .../benchmarks/helpers}/GearyBenchmark.kt | 2 + .../geary/benchmarks/misc/ComponentIdTest.kt | 2 +- .../geary/benchmarks/unpacking/Systems.kt | 52 +- .../benchmarks/unpacking/Unpack1Benchmark.kt | 11 +- .../benchmarks/unpacking/Unpack2Benchmark.kt | 11 +- .../benchmarks/unpacking/Unpack6Benchmark.kt | 46 +- .../geary/datatypes/ComponentList.kt | 1614 +++++++++++++++++ .../geary/engine/archetypes/Archetype.kt | 9 +- .../accessors/type/ComponentAccessor.kt | 9 +- .../geary/systems/builders/SystemBuilder.kt | 4 +- .../geary/systems/query/CachedQuery.kt | 56 +- .../geary/systems/query/QueriedEntity.kt | 5 +- .../geary/systems/query/QueryShorthands.kt | 43 +- .../AccessorDataModificationTests.kt | 8 +- .../systems/RelationMatchingSystemTest.kt | 25 +- 17 files changed, 1739 insertions(+), 168 deletions(-) rename geary-benchmarks/src/main/kotlin/{ => com/mineinabyss/geary/benchmarks/helpers}/GearyBenchmark.kt (81%) create mode 100644 geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/ComponentList.kt diff --git a/geary-benchmarks/build.gradle.kts b/geary-benchmarks/build.gradle.kts index 18fb0a7f2..75faa55c7 100644 --- a/geary-benchmarks/build.gradle.kts +++ b/geary-benchmarks/build.gradle.kts @@ -43,10 +43,10 @@ benchmark { } create("specific") { - include("EventCalls") - warmups = 1 - iterations = 1 - iterationTime = 3 + include("Unpack6") + warmups = 3 + iterations = 3 + iterationTime = 5 iterationTimeUnit = "sec" } } diff --git a/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/VelocitySystemBenchmark.kt b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/VelocitySystemBenchmark.kt index a72c80fe4..993692b48 100644 --- a/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/VelocitySystemBenchmark.kt +++ b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/VelocitySystemBenchmark.kt @@ -1,6 +1,6 @@ package com.mineinabyss.geary.benchmarks -import GearyBenchmark +import com.mineinabyss.geary.benchmarks.helpers.GearyBenchmark import com.mineinabyss.geary.benchmarks.helpers.oneMil import com.mineinabyss.geary.benchmarks.helpers.tenMil import com.mineinabyss.geary.helpers.entity diff --git a/geary-benchmarks/src/main/kotlin/GearyBenchmark.kt b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/helpers/GearyBenchmark.kt similarity index 81% rename from geary-benchmarks/src/main/kotlin/GearyBenchmark.kt rename to geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/helpers/GearyBenchmark.kt index d362fd27a..a581bdedf 100644 --- a/geary-benchmarks/src/main/kotlin/GearyBenchmark.kt +++ b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/helpers/GearyBenchmark.kt @@ -1,3 +1,5 @@ +package com.mineinabyss.geary.benchmarks.helpers + import com.mineinabyss.geary.modules.Geary import com.mineinabyss.geary.modules.TestEngineModule import com.mineinabyss.geary.modules.geary diff --git a/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/misc/ComponentIdTest.kt b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/misc/ComponentIdTest.kt index c148c6598..0ae88bcdd 100644 --- a/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/misc/ComponentIdTest.kt +++ b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/misc/ComponentIdTest.kt @@ -1,6 +1,6 @@ package com.mineinabyss.geary.benchmarks.misc -import GearyBenchmark +import com.mineinabyss.geary.benchmarks.helpers.GearyBenchmark import co.touchlab.kermit.Logger import co.touchlab.kermit.Severity import com.mineinabyss.geary.benchmarks.helpers.* diff --git a/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/unpacking/Systems.kt b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/unpacking/Systems.kt index 3e6f5c441..9360e213b 100644 --- a/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/unpacking/Systems.kt +++ b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/unpacking/Systems.kt @@ -3,51 +3,9 @@ package com.mineinabyss.geary.benchmarks.unpacking import com.mineinabyss.geary.benchmarks.helpers.* import com.mineinabyss.geary.modules.Geary import com.mineinabyss.geary.systems.query.GearyQuery +import com.mineinabyss.geary.systems.query.query -class Query1(world: Geary) : GearyQuery(world) { - val comp1 by get() -} - -class Query1Defaulting(world: Geary) : GearyQuery(world) { - val comp1 by get().orDefault { Comp1(0) } - override fun ensure() = this { has() } -} - -class Query2(world: Geary) : GearyQuery(world) { - val comp1 by get() - val comp2 by get() -} - -class Query6(world: Geary) : GearyQuery(world) { - val comp1 by get() - val comp2 by get() - val comp3 by get() - val comp4 by get() - val comp5 by get() - val comp6 by get() -} - - -class Query6WithoutDelegate(world: Geary) : GearyQuery(world) { - val comp1 = get() - val comp2 = get() - val comp3 = get() - val comp4 = get() - val comp5 = get() - val comp6 = get() - - override fun ensure() = this { - hasSet() - hasSet() - hasSet() - hasSet() - hasSet() - hasSet() - } -} - -fun Geary.systemOf1() = cache(::Query1) -fun Geary.systemOf1Defaulting() = cache(::Query1Defaulting) -fun Geary.systemOf2() = cache(::Query2) -fun Geary.systemOf6() = cache(::Query6) -fun Geary.systemOf6WithoutDelegate() = cache(::Query6WithoutDelegate) +fun Geary.systemOf1() = cache(query()) +fun Geary.systemOf1OrNull() = cache(query()) +fun Geary.systemOf2() = cache(query()) +fun Geary.systemOf6() = cache(query()) diff --git a/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/unpacking/Unpack1Benchmark.kt b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/unpacking/Unpack1Benchmark.kt index 642644475..ad7ff47b9 100644 --- a/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/unpacking/Unpack1Benchmark.kt +++ b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/unpacking/Unpack1Benchmark.kt @@ -1,7 +1,7 @@ package com.mineinabyss.geary.benchmarks.unpacking -import GearyBenchmark import com.mineinabyss.geary.benchmarks.helpers.Comp1 +import com.mineinabyss.geary.benchmarks.helpers.GearyBenchmark import com.mineinabyss.geary.benchmarks.helpers.tenMil import com.mineinabyss.geary.helpers.entity import org.openjdk.jmh.annotations.Benchmark @@ -11,7 +11,6 @@ import org.openjdk.jmh.annotations.State @State(Scope.Benchmark) class Unpack1Benchmark : GearyBenchmark() { - @Setup fun setUp() { repeat(tenMil) { @@ -23,8 +22,12 @@ class Unpack1Benchmark : GearyBenchmark() { @Benchmark fun unpack1of1Comp() { - systemOf1().forEach { - comp1 + systemOf1().forEach { (a) -> } + } + + @Benchmark + fun unpack1Nullable() { + systemOf1OrNull().forEach { (a) -> } } } diff --git a/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/unpacking/Unpack2Benchmark.kt b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/unpacking/Unpack2Benchmark.kt index 6b2a75a33..c09d32ee9 100644 --- a/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/unpacking/Unpack2Benchmark.kt +++ b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/unpacking/Unpack2Benchmark.kt @@ -1,8 +1,8 @@ package com.mineinabyss.geary.benchmarks.unpacking -import GearyBenchmark import com.mineinabyss.geary.benchmarks.helpers.Comp1 import com.mineinabyss.geary.benchmarks.helpers.Comp2 +import com.mineinabyss.geary.benchmarks.helpers.GearyBenchmark import com.mineinabyss.geary.benchmarks.helpers.tenMil import com.mineinabyss.geary.helpers.entity import org.openjdk.jmh.annotations.Benchmark @@ -24,16 +24,11 @@ class Unpack2Benchmark : GearyBenchmark() { @Benchmark fun unpack1of2Comp() { - systemOf1().forEach { - it.comp1 - } + systemOf2().forEach { (a) -> } } @Benchmark fun unpack2of2Comp() { - systemOf2().forEach { - it.comp1 - it.comp2 - } + systemOf2().forEach { (a, b) -> } } } diff --git a/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/unpacking/Unpack6Benchmark.kt b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/unpacking/Unpack6Benchmark.kt index 7e7808cd6..6b41632fb 100644 --- a/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/unpacking/Unpack6Benchmark.kt +++ b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/unpacking/Unpack6Benchmark.kt @@ -1,8 +1,8 @@ package com.mineinabyss.geary.benchmarks.unpacking -import GearyBenchmark import com.mineinabyss.geary.benchmarks.helpers.* import com.mineinabyss.geary.helpers.entity +import com.mineinabyss.geary.systems.query.query import org.openjdk.jmh.annotations.Benchmark import org.openjdk.jmh.annotations.Scope import org.openjdk.jmh.annotations.Setup @@ -10,7 +10,6 @@ import org.openjdk.jmh.annotations.State @State(Scope.Benchmark) class Unpack6Benchmark : GearyBenchmark() { - @Setup fun setUp() { repeat(tenMil) { @@ -27,47 +26,13 @@ class Unpack6Benchmark : GearyBenchmark() { @Benchmark fun unpack1of6Comp() { - systemOf1().forEach { - it.comp1 - } - } - - @Benchmark - fun unpack1of6CompWithUnoptimizedAccessor() { - systemOf1Defaulting().forEach { - it.comp1 + systemOf6().forEach { (a) -> } } @Benchmark fun unpack6of6Comp() { - systemOf6().forEach { - it.comp1 - it.comp2 - it.comp3 - it.comp4 - it.comp5 - it.comp6 - } - } - - @Benchmark - fun unpack1of6CompNoDelegate() { - systemOf6WithoutDelegate().forEach { - it.comp1.get(it) - } - } - - // This test gives ridiculous numbers, I think kotlin might just be optimizing some calls away that it can't with a delegate? - @Benchmark - fun unpack6of6CompNoDelegate() { - systemOf6WithoutDelegate().forEach { - it.comp1.get(it) - it.comp2.get(it) - it.comp3.get(it) - it.comp4.get(it) - it.comp5.get(it) - it.comp6.get(it) + systemOf6().forEach { (a, b, c, d, e, f) -> } } } @@ -75,9 +40,10 @@ class Unpack6Benchmark : GearyBenchmark() { fun main() { Unpack6Benchmark().apply { setUp() + val query = cache(query()) repeat(10000) { -// unpack1of6CompNoDelegate() - unpack6of6CompNoDelegate() + query.forEach { (a, b, c, d, e, f) -> + } } } } diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/ComponentList.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/ComponentList.kt new file mode 100644 index 000000000..183d51de0 --- /dev/null +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/ComponentList.kt @@ -0,0 +1,1614 @@ +@file:Suppress("NOTHING_TO_INLINE", "RedundantVisibilityModifier", "UNCHECKED_CAST") +@file:OptIn(ExperimentalContracts::class) +package com.mineinabyss.geary.datatypes;/* + * Copyright 2023 The Android Open Source Project + * + * 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. + */ +import androidx.collection.ScatterSet +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract +import kotlin.jvm.JvmField +import kotlin.jvm.JvmOverloads + +/** + * [ObjectList] is a [List]-like collection for reference types. It is optimized for fast + * access, avoiding virtual and interface method access. Methods avoid allocation whenever + * possible. For example [forEach] does not need allocate an [Iterator]. + * + * This implementation is not thread-safe: if multiple threads access this + * container concurrently, and one or more threads modify the structure of + * the list (insertion or removal for instance), the calling code must provide + * the appropriate synchronization. It is also not safe to mutate during reentrancy -- + * in the middle of a [forEach], for example. However, concurrent reads are safe. + * + * **Note** [List] access is available through [asList] when developers need access to the + * common API. + * + * It is best to use this for all internal implementations where a list of reference types + * is needed. Use [List] in public API to take advantage of the commonly-used interface. + * It is common to use [ObjectList] internally and use [asList] to get a [List] interface + * for interacting with public APIs. + * + * @see MutableComponentList + * @see FloatList + * @see IntList + * @eee LongList + */ +public sealed class ObjectList(initialCapacity: Int) { + @JvmField + var content: Array = if (initialCapacity == 0) { + EmptyArray + } else { + arrayOfNulls(initialCapacity) + } + + @Suppress("PropertyName") + @JvmField + @PublishedApi + internal var _size: Int = 0 + + /** + * The number of elements in the [ObjectList]. + */ + @get:androidx.annotation.IntRange(from = 0) + public val size: Int + get() = _size + + /** + * Returns the last valid index in the [ObjectList]. This can be `-1` when the list is empty. + */ + @get:androidx.annotation.IntRange(from = -1) + public inline val lastIndex: Int get() = _size - 1 + + /** + * Returns an [IntRange] of the valid indices for this [ObjectList]. + */ + public inline val indices: IntRange get() = 0 until _size + + /** + * Returns `true` if the collection has no elements in it. + */ + public fun none(): Boolean { + return isEmpty() + } + + /** + * Returns `true` if there's at least one element in the collection. + */ + public fun any(): Boolean { + return isNotEmpty() + } + + /** + * Returns `true` if any of the elements give a `true` return value for [predicate]. + */ + public inline fun any(predicate: (element: E) -> Boolean): Boolean { + contract { callsInPlace(predicate) } + forEach { + if (predicate(it)) { + return true + } + } + return false + } + + /** + * Returns `true` if any of the elements give a `true` return value for [predicate] while + * iterating in the reverse order. + */ + public inline fun reversedAny(predicate: (element: E) -> Boolean): Boolean { + contract { callsInPlace(predicate) } + forEachReversed { + if (predicate(it)) { + return true + } + } + return false + } + + /** + * Returns `true` if the [ObjectList] contains [element] or `false` otherwise. + */ + public operator fun contains(element: E): Boolean { + return indexOf(element) >= 0 + } + + /** + * Returns `true` if the [ObjectList] contains all elements in [elements] or `false` if + * one or more are missing. + */ + public fun containsAll(@Suppress("ArrayReturn") elements: Array): Boolean { + for (i in elements.indices) { + if (!contains(elements[i])) return false + } + return true + } + + /** + * Returns `true` if the [ObjectList] contains all elements in [elements] or `false` if + * one or more are missing. + */ + public fun containsAll(elements: List): Boolean { + for (i in elements.indices) { + if (!contains(elements[i])) return false + } + return true + } + + /** + * Returns `true` if the [ObjectList] contains all elements in [elements] or `false` if + * one or more are missing. + */ + public fun containsAll(elements: Iterable): Boolean { + elements.forEach { element -> + if (!contains(element)) return false + } + return true + } + + /** + * Returns `true` if the [ObjectList] contains all elements in [elements] or `false` if + * one or more are missing. + */ + public fun containsAll(elements: ObjectList): Boolean { + elements.forEach { element -> + if (!contains(element)) return false + } + return true + } + + /** + * Returns the number of elements in this list. + */ + public fun count(): Int = _size + + /** + * Counts the number of elements matching [predicate]. + * @return The number of elements in this list for which [predicate] returns true. + */ + public inline fun count(predicate: (element: E) -> Boolean): Int { + contract { callsInPlace(predicate) } + var count = 0 + forEach { if (predicate(it)) count++ } + return count + } + + /** + * Returns the first element in the [ObjectList] or throws a [NoSuchElementException] if + * it [isEmpty]. + */ + public fun first(): E { + if (isEmpty()) { + throw NoSuchElementException("ObjectList is empty.") + } + return content[0] as E + } + + /** + * Returns the first element in the [ObjectList] for which [predicate] returns `true` or + * throws [NoSuchElementException] if nothing matches. + * @see indexOfFirst + * @see firstOrNull + */ + public inline fun first(predicate: (element: E) -> Boolean): E { + contract { callsInPlace(predicate) } + forEach { element -> + if (predicate(element)) return element + } + throw NoSuchElementException("ObjectList contains no element matching the predicate.") + } + + /** + * Returns the first element in the [ObjectList] or `null` if it [isEmpty]. + */ + public inline fun firstOrNull(): E? = if (isEmpty()) null else get(0) + + /** + * Returns the first element in the [ObjectList] for which [predicate] returns `true` or + * `null` if nothing matches. + * @see indexOfFirst + */ + public inline fun firstOrNull(predicate: (element: E) -> Boolean): E? { + contract { callsInPlace(predicate) } + forEach { element -> + if (predicate(element)) return element + } + return null + } + + /** + * Accumulates values, starting with [initial], and applying [operation] to each element + * in the [ObjectList] in order. + * @param initial The value of `acc` for the first call to [operation] or return value if + * there are no elements in this list. + * @param operation function that takes current accumulator value and an element, and + * calculates the next accumulator value. + */ + public inline fun fold(initial: R, operation: (acc: R, element: E) -> R): R { + contract { callsInPlace(operation) } + var acc = initial + forEach { element -> + acc = operation(acc, element) + } + return acc + } + + /** + * Accumulates values, starting with [initial], and applying [operation] to each element + * in the [ObjectList] in order. + */ + public inline fun foldIndexed( + initial: R, + operation: (index: Int, acc: R, element: E) -> R + ): R { + contract { callsInPlace(operation) } + var acc = initial + forEachIndexed { i, element -> + acc = operation(i, acc, element) + } + return acc + } + + /** + * Accumulates values, starting with [initial], and applying [operation] to each element + * in the [ObjectList] in reverse order. + * @param initial The value of `acc` for the first call to [operation] or return value if + * there are no elements in this list. + * @param operation function that takes an element and the current accumulator value, and + * calculates the next accumulator value. + */ + public inline fun foldRight(initial: R, operation: (element: E, acc: R) -> R): R { + contract { callsInPlace(operation) } + var acc = initial + forEachReversed { element -> + acc = operation(element, acc) + } + return acc + } + + /** + * Accumulates values, starting with [initial], and applying [operation] to each element + * in the [ObjectList] in reverse order. + */ + public inline fun foldRightIndexed( + initial: R, + operation: (index: Int, element: E, acc: R) -> R + ): R { + contract { callsInPlace(operation) } + var acc = initial + forEachReversedIndexed { i, element -> + acc = operation(i, element, acc) + } + return acc + } + + /** + * Calls [block] for each element in the [ObjectList], in order. + * @param block will be executed for every element in the list, accepting an element from + * the list + */ + public inline fun forEach(block: (element: E) -> Unit) { + contract { callsInPlace(block) } + val content = content + for (i in 0 until _size) { + block(content[i] as E) + } + } + + /** + * Calls [block] for each element in the [ObjectList] along with its index, in order. + * @param block will be executed for every element in the list, accepting the index and + * the element at that index. + */ + public inline fun forEachIndexed(block: (index: Int, element: E) -> Unit) { + contract { callsInPlace(block) } + val content = content + for (i in 0 until _size) { + block(i, content[i] as E) + } + } + + /** + * Calls [block] for each element in the [ObjectList] in reverse order. + * @param block will be executed for every element in the list, accepting an element from + * the list + */ + public inline fun forEachReversed(block: (element: E) -> Unit) { + contract { callsInPlace(block) } + val content = content + for (i in _size - 1 downTo 0) { + block(content[i] as E) + } + } + + /** + * Calls [block] for each element in the [ObjectList] along with its index, in reverse + * order. + * @param block will be executed for every element in the list, accepting the index and + * the element at that index. + */ + public inline fun forEachReversedIndexed(block: (index: Int, element: E) -> Unit) { + contract { callsInPlace(block) } + val content = content + for (i in _size - 1 downTo 0) { + block(i, content[i] as E) + } + } + + /** + * Returns the element at the given [index] or throws [IndexOutOfBoundsException] if + * the [index] is out of bounds of this collection. + */ + public operator fun get(@androidx.annotation.IntRange(from = 0) index: Int): E { + if (index !in 0 until _size) { + throw IndexOutOfBoundsException("Index $index must be in 0..$lastIndex") + } + return content[index] as E + } + + /** + * Returns the element at the given [index] or throws [IndexOutOfBoundsException] if + * the [index] is out of bounds of this collection. + */ + public fun elementAt(@androidx.annotation.IntRange(from = 0) index: Int): E { + if (index !in 0 until _size) { + throw IndexOutOfBoundsException("Index $index must be in 0..$lastIndex") + } + return content[index] as E + } + + /** + * Returns the element at the given [index] or [defaultValue] if [index] is out of bounds + * of the collection. + * @param index The index of the element whose value should be returned + * @param defaultValue A lambda to call with [index] as a parameter to return a value at + * an index not in the list. + */ + public inline fun elementAtOrElse( + @androidx.annotation.IntRange(from = 0) index: Int, + defaultValue: (index: Int) -> E + ): E { + if (index !in 0 until _size) { + return defaultValue(index) + } + return content[index] as E + } + + /** + * Returns the index of [element] in the [ObjectList] or `-1` if [element] is not there. + */ + public fun indexOf(element: E): Int { + // Comparing with == for each element is slower than comparing with .equals(). + // We split the iteration for null and for non-null to speed it up. + // See ObjectListBenchmarkTest.contains() + if (element == null) { + forEachIndexed { i, item -> + if (item == null) { + return i + } + } + } else { + forEachIndexed { i, item -> + @Suppress("ReplaceCallWithBinaryOperator") + if (element.equals(item)) { + return i + } + } + } + return -1 + } + + /** + * Returns the index if the first element in the [ObjectList] for which [predicate] + * returns `true` or -1 if there was no element for which predicate returned `true`. + */ + public inline fun indexOfFirst(predicate: (element: E) -> Boolean): Int { + contract { callsInPlace(predicate) } + forEachIndexed { i, element -> + if (predicate(element)) { + return i + } + } + return -1 + } + + /** + * Returns the index if the last element in the [ObjectList] for which [predicate] + * returns `true` or -1 if there was no element for which predicate returned `true`. + */ + public inline fun indexOfLast(predicate: (element: E) -> Boolean): Int { + contract { callsInPlace(predicate) } + forEachReversedIndexed { i, element -> + if (predicate(element)) { + return i + } + } + return -1 + } + + /** + * Returns `true` if the [ObjectList] has no elements in it or `false` otherwise. + */ + public fun isEmpty(): Boolean = _size == 0 + + /** + * Returns `true` if there are elements in the [ObjectList] or `false` if it is empty. + */ + public fun isNotEmpty(): Boolean = _size != 0 + + /** + * Returns the last element in the [ObjectList] or throws a [NoSuchElementException] if + * it [isEmpty]. + */ + public fun last(): E { + if (isEmpty()) { + throw NoSuchElementException("ObjectList is empty.") + } + return content[lastIndex] as E + } + + /** + * Returns the last element in the [ObjectList] for which [predicate] returns `true` or + * throws [NoSuchElementException] if nothing matches. + * @see indexOfLast + * @see lastOrNull + */ + public inline fun last(predicate: (element: E) -> Boolean): E { + contract { callsInPlace(predicate) } + forEachReversed { element -> + if (predicate(element)) { + return element + } + } + throw NoSuchElementException("ObjectList contains no element matching the predicate.") + } + + /** + * Returns the last element in the [ObjectList] or `null` if it [isEmpty]. + */ + public inline fun lastOrNull(): E? = if (isEmpty()) null else content[lastIndex] as E + + /** + * Returns the last element in the [ObjectList] for which [predicate] returns `true` or + * `null` if nothing matches. + * @see indexOfLast + */ + public inline fun lastOrNull(predicate: (element: E) -> Boolean): E? { + contract { callsInPlace(predicate) } + forEachReversed { element -> + if (predicate(element)) { + return element + } + } + return null + } + + /** + * Returns the index of the last element in the [ObjectList] that is the same as + * [element] or `-1` if no elements match. + */ + public fun lastIndexOf(element: E): Int { + // Comparing with == for each element is slower than comparing with .equals(). + // We split the iteration for null and for non-null to speed it up. + // See ObjectListBenchmarkTest.contains() + if (element == null) { + forEachReversedIndexed { i, item -> + if (item == null) { + return i + } + } + } else { + forEachReversedIndexed { i, item -> + @Suppress("ReplaceCallWithBinaryOperator") + if (element.equals(item)) { + return i + } + } + } + return -1 + } + + /** + * Creates a String from the elements separated by [separator] and using [prefix] before + * and [postfix] after, if supplied. + * + * When a non-negative value of [limit] is provided, a maximum of [limit] items are used + * to generate the string. If the collection holds more than [limit] items, the string + * is terminated with [truncated]. + * + * [transform] may be supplied to convert each element to a custom String. + */ + @JvmOverloads + public fun joinToString( + separator: CharSequence = ", ", + prefix: CharSequence = "", + postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name + limit: Int = -1, + truncated: CharSequence = "...", + transform: ((E) -> CharSequence)? = null + ): String = buildString { + append(prefix) + this@ObjectList.forEachIndexed { index, element -> + if (index == limit) { + append(truncated) + return@buildString + } + if (index != 0) { + append(separator) + } + if (transform == null) { + append(element) + } else { + append(transform(element)) + } + } + append(postfix) + } + + /** + * Returns a [List] view into the [ObjectList]. All access to the collection will be + * less efficient and abides by the allocation requirements of the [List]. For example, + * [List.forEach] will allocate an iterator. All access will go through the more expensive + * interface calls. Critical performance areas should use the [ObjectList] API rather than + * [List] API, when possible. + */ + public abstract fun asList(): List + + /** + * Returns a hash code based on the contents of the [ObjectList]. + */ + override fun hashCode(): Int { + var hashCode = 0 + forEach { element -> + hashCode += 31 * element.hashCode() + } + return hashCode + } + + /** + * Returns `true` if [other] is a [ObjectList] and the contents of this and [other] are the + * same. + */ + override fun equals(other: Any?): Boolean { + if (other !is ObjectList<*> || other._size != _size) { + return false + } + val content = content + val otherContent = other.content + for (i in indices) { + if (content[i] != otherContent[i]) { + return false + } + } + return true + } + + /** + * Returns a String representation of the list, surrounded by "[]" and each element + * separated by ", ". + */ + override fun toString(): String = joinToString(prefix = "[", postfix = "]") { element -> + if (element === this) { + "(this)" + } else { + element.toString() + } + } +} + +/** + * [MutableComponentList] is a [MutableList]-like collection for reference types. It is optimized + * for fast access, avoiding virtual and interface method access. Methods avoid allocation + * whenever possible. For example [forEach] does not need allocate an [Iterator]. + * + * This implementation is not thread-safe: if multiple threads access this + * container concurrently, and one or more threads modify the structure of + * the list (insertion or removal for instance), the calling code must provide + * the appropriate synchronization. It is also not safe to mutate during reentrancy -- + * in the middle of a [forEach], for example. However, concurrent reads are safe. + * + * **Note** [List] access is available through [asList] when developers need access to the + * common API. + + * **Note** [MutableList] access is available through [asMutableList] when developers need + * access to the common API. + * + * It is best to use this for all internal implementations where a list of reference types + * is needed. Use [MutableList] in public API to take advantage of the commonly-used interface. + * It is common to use [MutableComponentList] internally and use [asMutableList] or [asList] + * to get a [MutableList] or [List] interface for interacting with public APIs. + * + * @see ObjectList + * @see MutableFloatList + * @see MutableIntList + * @eee MutableLongList + */ +public class MutableComponentList( + initialCapacity: Int = 16 +) : ObjectList(initialCapacity) { + private var list: ObjectListMutableList? = null + + /** + * Returns the total number of elements that can be held before the [MutableComponentList] must + * grow. + * + * @see ensureCapacity + */ + public inline val capacity: Int + get() = content.size + + /** + * Adds [element] to the [MutableComponentList] and returns `true`. + */ + public fun add(element: E): Boolean { + ensureCapacity(_size + 1) + content[_size] = element + _size++ + return true + } + + /** + * Adds [element] to the [MutableComponentList] at the given [index], shifting over any + * elements at [index] and after, if any. + * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive + */ + public fun add(@androidx.annotation.IntRange(from = 0) index: Int, element: E) { + if (index !in 0.._size) { + throw IndexOutOfBoundsException("Index $index must be in 0..$_size") + } + ensureCapacity(_size + 1) + val content = content + if (index != _size) { + content.copyInto( + destination = content, + destinationOffset = index + 1, + startIndex = index, + endIndex = _size + ) + } + content[index] = element + _size++ + } + + /** + * Adds all [elements] to the [MutableComponentList] at the given [index], shifting over any + * elements at [index] and after, if any. + * @return `true` if the [MutableComponentList] was changed or `false` if [elements] was empty + * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive. + */ + public fun addAll( + @androidx.annotation.IntRange(from = 0) index: Int, + @Suppress("ArrayReturn") elements: Array + ): Boolean { + if (index !in 0.._size) { + throw IndexOutOfBoundsException("Index $index must be in 0..$_size") + } + if (elements.isEmpty()) return false + ensureCapacity(_size + elements.size) + val content = content + if (index != _size) { + content.copyInto( + destination = content, + destinationOffset = index + elements.size, + startIndex = index, + endIndex = _size + ) + } + elements.copyInto(content, index) + _size += elements.size + return true + } + + /** + * Adds all [elements] to the [MutableComponentList] at the given [index], shifting over any + * elements at [index] and after, if any. + * @return `true` if the [MutableComponentList] was changed or `false` if [elements] was empty + * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive. + */ + public fun addAll( + @androidx.annotation.IntRange(from = 0) index: Int, + elements: Collection + ): Boolean { + if (index !in 0.._size) { + throw IndexOutOfBoundsException("Index $index must be in 0..$_size") + } + if (elements.isEmpty()) return false + ensureCapacity(_size + elements.size) + val content = content + if (index != _size) { + content.copyInto( + destination = content, + destinationOffset = index + elements.size, + startIndex = index, + endIndex = _size + ) + } + elements.forEachIndexed { i, element -> + content[index + i] = element + } + _size += elements.size + return true + } + + /** + * Adds all [elements] to the [MutableComponentList] at the given [index], shifting over any + * elements at [index] and after, if any. + * @return `true` if the [MutableComponentList] was changed or `false` if [elements] was empty + * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive + */ + public fun addAll( + @androidx.annotation.IntRange(from = 0) index: Int, + elements: ObjectList + ): Boolean { + if (index !in 0.._size) { + throw IndexOutOfBoundsException("Index $index must be in 0..$_size") + } + if (elements.isEmpty()) return false + ensureCapacity(_size + elements._size) + val content = content + if (index != _size) { + content.copyInto( + destination = content, + destinationOffset = index + elements._size, + startIndex = index, + endIndex = _size + ) + } + elements.content.copyInto( + destination = content, + destinationOffset = index, + startIndex = 0, + endIndex = elements._size + ) + _size += elements._size + return true + } + + /** + * Adds all [elements] to the end of the [MutableComponentList] and returns `true` if the + * [MutableComponentList] was changed or `false` if [elements] was empty. + */ + public fun addAll(elements: ObjectList): Boolean { + val oldSize = _size + plusAssign(elements) + return oldSize != _size + } + + /** + * Adds all [elements] to the end of the [MutableComponentList] and returns `true` if the + * [MutableComponentList] was changed or `false` if [elements] was empty. + */ + public fun addAll(elements: ScatterSet): Boolean { + val oldSize = _size + plusAssign(elements) + return oldSize != _size + } + + /** + * Adds all [elements] to the end of the [MutableComponentList] and returns `true` if the + * [MutableComponentList] was changed or `false` if [elements] was empty. + */ + public fun addAll(@Suppress("ArrayReturn") elements: Array): Boolean { + val oldSize = _size + plusAssign(elements) + return oldSize != _size + } + + /** + * Adds all [elements] to the end of the [MutableComponentList] and returns `true` if the + * [MutableComponentList] was changed or `false` if [elements] was empty. + */ + public fun addAll(elements: List): Boolean { + val oldSize = _size + plusAssign(elements) + return oldSize != _size + } + + /** + * Adds all [elements] to the end of the [MutableComponentList] and returns `true` if the + * [MutableComponentList] was changed or `false` if [elements] was empty. + */ + public fun addAll(elements: Iterable): Boolean { + val oldSize = _size + plusAssign(elements) + return oldSize != _size + } + + /** + * Adds all [elements] to the end of the [MutableComponentList] and returns `true` if the + * [MutableComponentList] was changed or `false` if [elements] was empty. + */ + public fun addAll(elements: Sequence): Boolean { + val oldSize = _size + plusAssign(elements) + return oldSize != _size + } + + /** + * Adds all [elements] to the end of the [MutableComponentList]. + */ + public operator fun plusAssign(elements: ObjectList) { + if (elements.isEmpty()) return + ensureCapacity(_size + elements._size) + val content = content + elements.content.copyInto( + destination = content, + destinationOffset = _size, + startIndex = 0, + endIndex = elements._size + ) + _size += elements._size + } + + /** + * Adds all [elements] to the end of the [MutableComponentList]. + */ + public operator fun plusAssign(elements: ScatterSet) { + if (elements.isEmpty()) return + ensureCapacity(_size + elements.size) + elements.forEach { element -> + plusAssign(element) + } + } + + /** + * Adds all [elements] to the end of the [MutableComponentList]. + */ + public operator fun plusAssign(@Suppress("ArrayReturn") elements: Array) { + if (elements.isEmpty()) return + ensureCapacity(_size + elements.size) + val content = content + elements.copyInto(content, _size) + _size += elements.size + } + + /** + * Adds all [elements] to the end of the [MutableComponentList]. + */ + public operator fun plusAssign(elements: List) { + if (elements.isEmpty()) return + val size = _size + ensureCapacity(size + elements.size) + val content = content + for (i in elements.indices) { + content[i + size] = elements[i] + } + _size += elements.size + } + + /** + * Adds all [elements] to the end of the [MutableComponentList]. + */ + public operator fun plusAssign(elements: Iterable) { + elements.forEach { element -> + plusAssign(element) + } + } + + /** + * Adds all [elements] to the end of the [MutableComponentList]. + */ + public operator fun plusAssign(elements: Sequence) { + elements.forEach { element -> + plusAssign(element) + } + } + + /** + * Removes all elements in the [MutableComponentList]. The storage isn't released. + * @see trim + */ + public fun clear() { + content.fill(null, fromIndex = 0, toIndex = _size) + _size = 0 + } + + /** + * Reduces the internal storage. If [capacity] is greater than [minCapacity] and [size], the + * internal storage is reduced to the maximum of [size] and [minCapacity]. + * @see ensureCapacity + */ + public fun trim(minCapacity: Int = _size) { + val minSize = maxOf(minCapacity, _size) + if (capacity > minSize) { + content = content.copyOf(minSize) + } + } + + /** + * Ensures that there is enough space to store [capacity] elements in the [MutableComponentList]. + * @see trim + */ + public fun ensureCapacity(capacity: Int) { + val oldContent = content + if (oldContent.size < capacity) { + val newSize = maxOf(capacity, oldContent.size * 3 / 2) + content = oldContent.copyOf(newSize) + } + } + + /** + * [add] [element] to the [MutableComponentList]. + */ + public inline operator fun plusAssign(element: E) { + add(element) + } + + /** + * [remove] [element] from the [MutableComponentList] + */ + public inline operator fun minusAssign(element: E) { + remove(element) + } + + /** + * Removes [element] from the [MutableComponentList]. If [element] was in the [MutableComponentList] + * and was removed, `true` will be returned, or `false` will be returned if the element + * was not found. + */ + public fun remove(element: E): Boolean { + val index = indexOf(element) + if (index >= 0) { + removeAt(index) + return true + } + return false + } + + /** + * Removes all elements in this list for which [predicate] returns `true`. + */ + public inline fun removeIf(predicate: (element: E) -> Boolean) { + var gap = 0 + val size = _size + val content = content + for (i in indices) { + content[i - gap] = content[i] + if (predicate(content[i] as E)) { + gap++ + } + } + content.fill(null, fromIndex = size - gap, toIndex = size) + _size -= gap + } + + /** + * Removes all [elements] from the [MutableComponentList] and returns `true` if anything was removed. + */ + public fun removeAll(@Suppress("ArrayReturn") elements: Array): Boolean { + val initialSize = _size + for (i in elements.indices) { + remove(elements[i]) + } + return initialSize != _size + } + + /** + * Removes all [elements] from the [MutableComponentList] and returns `true` if anything was removed. + */ + public fun removeAll(elements: ObjectList): Boolean { + val initialSize = _size + minusAssign(elements) + return initialSize != _size + } + + /** + * Removes all [elements] from the [MutableComponentList] and returns `true` if anything was removed. + */ + public fun removeAll(elements: ScatterSet): Boolean { + val initialSize = _size + minusAssign(elements) + return initialSize != _size + } + + /** + * Removes all [elements] from the [MutableComponentList] and returns `true` if anything was removed. + */ + public fun removeAll(elements: List): Boolean { + val initialSize = _size + minusAssign(elements) + return initialSize != _size + } + + /** + * Removes all [elements] from the [MutableComponentList] and returns `true` if anything was removed. + */ + public fun removeAll(elements: Iterable): Boolean { + val initialSize = _size + minusAssign(elements) + return initialSize != _size + } + + /** + * Removes all [elements] from the [MutableComponentList] and returns `true` if anything was removed. + */ + public fun removeAll(elements: Sequence): Boolean { + val initialSize = _size + minusAssign(elements) + return initialSize != _size + } + + /** + * Removes all [elements] from the [MutableComponentList]. + */ + public operator fun minusAssign(@Suppress("ArrayReturn") elements: Array) { + elements.forEach { element -> + minusAssign(element) + } + } + + /** + * Removes all [elements] from the [MutableComponentList]. + */ + public operator fun minusAssign(elements: ObjectList) { + elements.forEach { element -> + minusAssign(element) + } + } + + /** + * Removes all [elements] from the [MutableComponentList]. + */ + public operator fun minusAssign(elements: ScatterSet) { + elements.forEach { element -> + minusAssign(element) + } + } + + /** + * Removes all [elements] from the [MutableComponentList]. + */ + public operator fun minusAssign(elements: List) { + for (i in elements.indices) { + minusAssign(elements[i]) + } + } + + /** + * Removes all [elements] from the [MutableComponentList]. + */ + public operator fun minusAssign(elements: Iterable) { + elements.forEach { element -> + minusAssign(element) + } + } + + /** + * Removes all [elements] from the [MutableComponentList]. + */ + public operator fun minusAssign(elements: Sequence) { + elements.forEach { element -> + minusAssign(element) + } + } + + /** + * Removes the element at the given [index] and returns it. + * @throws IndexOutOfBoundsException if [index] isn't between 0 and [lastIndex], inclusive + */ + public fun removeAt(@androidx.annotation.IntRange(from = 0) index: Int): E { + if (index !in 0 until _size) { + throw IndexOutOfBoundsException("Index $index must be in 0..$lastIndex") + } + val content = content + val element = content[index] + if (index != lastIndex) { + content.copyInto( + destination = content, + destinationOffset = index, + startIndex = index + 1, + endIndex = _size + ) + } + _size-- + content[_size] = null + return element as E + } + + /** + * Removes elements from index [start] (inclusive) to [end] (exclusive). + * @throws IndexOutOfBoundsException if [start] or [end] isn't between 0 and [size], inclusive + * @throws IllegalArgumentException if [start] is greater than [end] + */ + public fun removeRange( + @androidx.annotation.IntRange(from = 0) start: Int, + @androidx.annotation.IntRange(from = 0) end: Int + ) { + if (start !in 0.._size || end !in 0.._size) { + throw IndexOutOfBoundsException("Start ($start) and end ($end) must be in 0..$_size") + } + if (end < start) { + throw IllegalArgumentException("Start ($start) is more than end ($end)") + } + if (end != start) { + if (end < _size) { + content.copyInto( + destination = content, + destinationOffset = start, + startIndex = end, + endIndex = _size + ) + } + val newSize = _size - (end - start) + content.fill(null, fromIndex = newSize, toIndex = _size) + _size = newSize + } + } + + /** + * Keeps only [elements] in the [MutableComponentList] and removes all other values. + * @return `true` if the [MutableComponentList] has changed. + */ + public fun retainAll(@Suppress("ArrayReturn") elements: Array): Boolean { + val initialSize = _size + val content = content + for (i in lastIndex downTo 0) { + val element = content[i] + if (elements.indexOf(element) < 0) { + removeAt(i) + } + } + return initialSize != _size + } + + /** + * Keeps only [elements] in the [MutableComponentList] and removes all other values. + * @return `true` if the [MutableComponentList] has changed. + */ + public fun retainAll(elements: ObjectList): Boolean { + val initialSize = _size + val content = content + for (i in lastIndex downTo 0) { + val element = content[i] as E + if (element !in elements) { + removeAt(i) + } + } + return initialSize != _size + } + + /** + * Keeps only [elements] in the [MutableComponentList] and removes all other values. + * @return `true` if the [MutableComponentList] has changed. + */ + public fun retainAll(elements: Collection): Boolean { + val initialSize = _size + val content = content + for (i in lastIndex downTo 0) { + val element = content[i] as E + if (element !in elements) { + removeAt(i) + } + } + return initialSize != _size + } + + /** + * Keeps only [elements] in the [MutableComponentList] and removes all other values. + * @return `true` if the [MutableComponentList] has changed. + */ + public fun retainAll(elements: Iterable): Boolean { + val initialSize = _size + val content = content + for (i in lastIndex downTo 0) { + val element = content[i] as E + if (element !in elements) { + removeAt(i) + } + } + return initialSize != _size + } + + /** + * Keeps only [elements] in the [MutableComponentList] and removes all other values. + * @return `true` if the [MutableComponentList] has changed. + */ + public fun retainAll(elements: Sequence): Boolean { + val initialSize = _size + val content = content + for (i in lastIndex downTo 0) { + val element = content[i] as E + if (element !in elements) { + removeAt(i) + } + } + return initialSize != _size + } + + /** + * Sets the value at [index] to [element]. + * @return the previous value set at [index] + * @throws IndexOutOfBoundsException if [index] isn't between 0 and [lastIndex], inclusive + */ + public operator fun set( + @androidx.annotation.IntRange(from = 0) index: Int, + element: E + ): E { + if (index !in 0 until _size) { + throw IndexOutOfBoundsException("set index $index must be between 0 .. $lastIndex") + } + val content = content + val old = content[index] + content[index] = element + return old as E + } + + override fun asList(): List = asMutableList() + + /** + * Returns a [MutableList] view into the [MutableComponentList]. All access to the collection + * will be less efficient and abides by the allocation requirements of the + * [MutableList]. For example, [MutableList.forEach] will allocate an iterator. + * All access will go through the more expensive interface calls. Critical performance + * areas should use the [MutableComponentList] API rather than [MutableList] API, when possible. + */ + public fun asMutableList(): MutableList = list ?: ObjectListMutableList(this).also { + list = it + } + + private class MutableObjectListIterator( + private val list: MutableList, + index: Int + ) : MutableListIterator { + private var prevIndex = index - 1 + + override fun hasNext(): Boolean { + return prevIndex < list.size - 1 + } + + override fun next(): T { + return list[++prevIndex] + } + + override fun remove() { + list.removeAt(prevIndex) + prevIndex-- + } + + override fun hasPrevious(): Boolean { + return prevIndex >= 0 + } + + override fun nextIndex(): Int { + return prevIndex + 1 + } + + override fun previous(): T { + return list[prevIndex--] + } + + override fun previousIndex(): Int { + return prevIndex + } + + override fun add(element: T) { + list.add(++prevIndex, element) + } + + override fun set(element: T) { + list[prevIndex] = element + } + } + + /** + * [MutableList] implementation for a [MutableComponentList], used in [asMutableList]. + */ + private class ObjectListMutableList( + private val objectList: MutableComponentList + ) : MutableList { + override val size: Int + get() = objectList.size + + override fun contains(element: T): Boolean = objectList.contains(element) + + override fun containsAll(elements: Collection): Boolean = + objectList.containsAll(elements) + + override fun get(index: Int): T { + checkIndex(index) + return objectList[index] + } + + override fun indexOf(element: T): Int = objectList.indexOf(element) + + override fun isEmpty(): Boolean = objectList.isEmpty() + + override fun iterator(): MutableIterator = MutableObjectListIterator(this, 0) + + override fun lastIndexOf(element: T): Int = objectList.lastIndexOf(element) + + override fun add(element: T): Boolean = objectList.add(element) + + override fun add(index: Int, element: T) = objectList.add(index, element) + + override fun addAll(index: Int, elements: Collection): Boolean = + objectList.addAll(index, elements) + + override fun addAll(elements: Collection): Boolean = objectList.addAll(elements) + + override fun clear() = objectList.clear() + + override fun listIterator(): MutableListIterator = MutableObjectListIterator(this, 0) + + override fun listIterator(index: Int): MutableListIterator = + MutableObjectListIterator(this, index) + + override fun remove(element: T): Boolean = objectList.remove(element) + + override fun removeAll(elements: Collection): Boolean = objectList.removeAll(elements) + + override fun removeAt(index: Int): T { + checkIndex(index) + return objectList.removeAt(index) + } + + override fun retainAll(elements: Collection): Boolean = objectList.retainAll(elements) + + override fun set(index: Int, element: T): T { + checkIndex(index) + return objectList.set(index, element) + } + + override fun subList(fromIndex: Int, toIndex: Int): MutableList { + checkSubIndex(fromIndex, toIndex) + return SubList(this, fromIndex, toIndex) + } + } + + /** + * A view into an underlying [MutableList] that directly accesses the underlying [MutableList]. + * This is important for the implementation of [List.subList]. A change to the [SubList] + * also changes the referenced [MutableList]. + */ + private class SubList( + private val list: MutableList, + private val start: Int, + private var end: Int + ) : MutableList { + override val size: Int + get() = end - start + + override fun contains(element: T): Boolean { + for (i in start until end) { + if (list[i] == element) { + return true + } + } + return false + } + + override fun containsAll(elements: Collection): Boolean { + elements.forEach { + if (!contains(it)) { + return false + } + } + return true + } + + override fun get(index: Int): T { + checkIndex(index) + return list[index + start] + } + + override fun indexOf(element: T): Int { + for (i in start until end) { + if (list[i] == element) { + return i - start + } + } + return -1 + } + + override fun isEmpty(): Boolean = end == start + + override fun iterator(): MutableIterator = MutableObjectListIterator(this, 0) + + override fun lastIndexOf(element: T): Int { + for (i in end - 1 downTo start) { + if (list[i] == element) { + return i - start + } + } + return -1 + } + + override fun add(element: T): Boolean { + list.add(end++, element) + return true + } + + override fun add(index: Int, element: T) { + list.add(index + start, element) + end++ + } + + override fun addAll(index: Int, elements: Collection): Boolean { + list.addAll(index + start, elements) + end += elements.size + return elements.size > 0 + } + + override fun addAll(elements: Collection): Boolean { + list.addAll(end, elements) + end += elements.size + return elements.size > 0 + } + + override fun clear() { + for (i in end - 1 downTo start) { + list.removeAt(i) + } + end = start + } + + override fun listIterator(): MutableListIterator = MutableObjectListIterator(this, 0) + + override fun listIterator(index: Int): MutableListIterator = + MutableObjectListIterator(this, index) + + override fun remove(element: T): Boolean { + for (i in start until end) { + if (list[i] == element) { + list.removeAt(i) + end-- + return true + } + } + return false + } + + override fun removeAll(elements: Collection): Boolean { + val originalEnd = end + elements.forEach { + remove(it) + } + return originalEnd != end + } + + override fun removeAt(index: Int): T { + checkIndex(index) + val element = list.removeAt(index + start) + end-- + return element + } + + override fun retainAll(elements: Collection): Boolean { + val originalEnd = end + for (i in end - 1 downTo start) { + val element = list[i] + if (element !in elements) { + list.removeAt(i) + end-- + } + } + return originalEnd != end + } + + override fun set(index: Int, element: T): T { + checkIndex(index) + return list.set(index + start, element) + } + + override fun subList(fromIndex: Int, toIndex: Int): MutableList { + checkSubIndex(fromIndex, toIndex) + return SubList(this, fromIndex, toIndex) + } + } +} + +private fun List<*>.checkIndex(index: Int) { + val size = size + if (index < 0 || index >= size) { + throw IndexOutOfBoundsException("Index $index is out of bounds. " + + "The list has $size elements.") + } +} + +private fun List<*>.checkSubIndex(fromIndex: Int, toIndex: Int) { + val size = size + if (fromIndex > toIndex) { + throw IllegalArgumentException("Indices are out of order. fromIndex ($fromIndex) is " + + "greater than toIndex ($toIndex).") + } + if (fromIndex < 0) { + throw IndexOutOfBoundsException("fromIndex ($fromIndex) is less than 0.") + } + if (toIndex > size) { + throw IndexOutOfBoundsException( + "toIndex ($toIndex) is more than than the list size ($size)" + ) + } +} + +// Empty array used when nothing is allocated +private val EmptyArray = arrayOfNulls(0) + +private val EmptyObjectList: ObjectList = MutableComponentList(0) + +/** + * @return a read-only [ObjectList] with nothing in it. + */ +public fun emptyObjectList(): ObjectList = EmptyObjectList as ObjectList + +/** + * @return a read-only [ObjectList] with nothing in it. + */ +public fun objectListOf(): ObjectList = EmptyObjectList as ObjectList + +/** + * @return a new read-only [ObjectList] with [element1] as the only element in the list. + */ +public fun objectListOf(element1: E): ObjectList = mutableComponentListOf(element1) + +/** + * @return a new read-only [ObjectList] with 2 elements, [element1] and [element2], in order. + */ +public fun objectListOf(element1: E, element2: E): ObjectList = + mutableComponentListOf(element1, element2) + +/** + * @return a new read-only [ObjectList] with 3 elements, [element1], [element2], and [element3], + * in order. + */ +public fun objectListOf(element1: E, element2: E, element3: E): ObjectList = + mutableComponentListOf(element1, element2, element3) + +/** + * @return a new read-only [ObjectList] with [elements] in order. + */ +public fun objectListOf(vararg elements: E): ObjectList = + MutableComponentList(elements.size).apply { plusAssign(elements as Array) } + +/** + * @return a new empty [MutableComponentList] with the default capacity. + */ +public inline fun mutableComponentListOf(): MutableComponentList = MutableComponentList() + +/** + * @return a new [MutableComponentList] with [element1] as the only element in the list. + */ +public fun mutableComponentListOf(element1: E): MutableComponentList { + val list = MutableComponentList(1) + list += element1 + return list +} + +/** + * @return a new [MutableComponentList] with 2 elements, [element1] and [element2], in order. + */ +public fun mutableComponentListOf(element1: E, element2: E): MutableComponentList { + val list = MutableComponentList(2) + list += element1 + list += element2 + return list +} + +/** + * @return a new [MutableComponentList] with 3 elements, [element1], [element2], and [element3], + * in order. + */ +public fun mutableComponentListOf(element1: E, element2: E, element3: E): MutableComponentList { + val list = MutableComponentList(3) + list += element1 + list += element2 + list += element3 + return list +} + +/** + * @return a new [MutableComponentList] with the given elements, in order. + */ +public inline fun mutableComponentListOf(vararg elements: E): MutableComponentList = + MutableComponentList(elements.size).apply { plusAssign(elements as Array) } diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/Archetype.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/Archetype.kt index f4baf7ff5..0a78b0eb0 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/Archetype.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/Archetype.kt @@ -7,6 +7,7 @@ import com.mineinabyss.geary.datatypes.maps.ArrayTypeMap import com.mineinabyss.geary.helpers.* import com.mineinabyss.geary.observers.events.* import com.mineinabyss.geary.systems.accessors.RelationWithData +import kotlin.jvm.JvmField /** * Archetypes store a list of entities with the same [EntityType], and provide functions to @@ -27,9 +28,6 @@ class Archetype internal constructor( /** The entity ids in this archetype. Indices are the same as [componentData]'s sub-lists. */ private val ids = mutableLongListOf() - @PublishedApi - internal var isIterating: Boolean = false - private var unregistered: Boolean = false // This is way slower as a Boolean? because of boxing @@ -41,8 +39,9 @@ class Archetype internal constructor( internal val dataHoldingType: EntityType = type.filter { it.holdsData() } /** An outer list with indices for component ids, and sub-lists with data indexed by entity [ids]. */ - internal val componentData: Array> = - Array(dataHoldingType.size) { mutableObjectListOf() } + @JvmField + internal val componentData: Array> = + Array(dataHoldingType.size) { mutableComponentListOf() } /** Edges to other archetypes where a single component has been added. */ internal val componentAddEdges = LongSparseArray() diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/type/ComponentAccessor.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/type/ComponentAccessor.kt index 81f1dacff..bfbc9be2b 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/type/ComponentAccessor.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/type/ComponentAccessor.kt @@ -1,7 +1,5 @@ package com.mineinabyss.geary.systems.accessors.type -import androidx.collection.MutableObjectList -import androidx.collection.mutableObjectListOf import com.mineinabyss.geary.annotations.optin.UnsafeAccessors import com.mineinabyss.geary.datatypes.ComponentId import com.mineinabyss.geary.datatypes.family.family @@ -16,17 +14,18 @@ import com.mineinabyss.geary.systems.query.Query class ComponentAccessor( comp: ComponentProvider, override val originalAccessor: Accessor?, - val id: ComponentId + val id: ComponentId, ) : ReadWriteAccessor, FamilyMatching { override val family = family { hasSet(id) } private var cachedIndex = -1 - private var cachedDataArray: MutableObjectList = mutableObjectListOf() + private var cachedDataArray: Array = arrayOf() as Array fun updateCache(archetype: Archetype) { cachedIndex = archetype.indexOf(id) @Suppress("UNCHECKED_CAST") - if (cachedIndex != -1) cachedDataArray = archetype.componentData[cachedIndex] as MutableObjectList + if (cachedIndex != -1) cachedDataArray = + archetype.componentData[cachedIndex].content as Array } override fun get(query: Query): T { diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/builders/SystemBuilder.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/builders/SystemBuilder.kt index 6af90a634..29f8cb048 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/builders/SystemBuilder.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/builders/SystemBuilder.kt @@ -21,13 +21,13 @@ data class SystemBuilder( return copy(interval = interval) } - inline fun exec(crossinline run: T.(T) -> Unit): TrackedSystem<*> { + inline fun exec(crossinline run: (T) -> Unit): TrackedSystem<*> { val onTick: CachedQuery.() -> Unit = { forEach { run(it) } } val system = System(name, query, onTick, interval) return pipeline.addSystem(system) } - inline fun defer(crossinline run: T.(T) -> R): DeferredSystemBuilder { + inline fun defer(crossinline run: (T) -> R): DeferredSystemBuilder { val onTick: CachedQuery.() -> List> = { mapWithEntity { run(it) } } diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/query/CachedQuery.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/query/CachedQuery.kt index 8d2e26d78..437c0a186 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/query/CachedQuery.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/query/CachedQuery.kt @@ -1,12 +1,10 @@ package com.mineinabyss.geary.systems.query +import androidx.collection.LongList import androidx.collection.mutableLongListOf import com.mineinabyss.geary.annotations.optin.ExperimentalGearyApi import com.mineinabyss.geary.annotations.optin.UnsafeAccessors -import com.mineinabyss.geary.datatypes.Entity -import com.mineinabyss.geary.datatypes.EntityArray -import com.mineinabyss.geary.datatypes.GearyEntity -import com.mineinabyss.geary.datatypes.toEntityArray +import com.mineinabyss.geary.datatypes.* import com.mineinabyss.geary.engine.archetypes.Archetype import com.mineinabyss.geary.engine.archetypes.ArchetypeProvider import com.mineinabyss.geary.helpers.fastForEach @@ -23,34 +21,33 @@ class CachedQuery internal constructor(val query: T) { * Use [apply] on the query to use its accessors. * */ @OptIn(UnsafeAccessors::class) - inline fun forEach(crossinline run: T.(T) -> Unit) { + inline fun forEach(run: (T) -> Unit) { val matched = matchedArchetypes var n = 0 val size = matched.size // Get size ahead of time to avoid rerunning on entities that end up in new archetypes val accessors = cachingAccessors +// val query = query while (n < size) { val archetype = matched[n] - archetype.isIterating = true // We disallow entity archetype modifications while iterating, but allow creating new entities. // These will always end up at the end of the archetype list, so we just don't iterate over them. val upTo = archetype.size var row = 0 + query.row = 0 query.archetype = archetype accessors.fastForEach { it.updateCache(archetype) } -// try { - while (row < upTo) { - query.row = row - run(query, query) - row++ - } - n++ -// } finally { -// archetype.isIterating = false -// } + while (row < upTo) { + run(query) + query.row++ + row++ + } + n++ } } + fun Archetype.getData() = componentData + /** * Allows collecting values as a sequence under some rules. Slower than [forEach] or functions directly on the runner like [map], [any], [find] * since an iterator must be used, but can be much faster if terminating early (ex. `take(5)`). @@ -93,7 +90,6 @@ class CachedQuery internal constructor(val query: T) { upTo = archetype.size query.archetype = archetype accessors.fastForEach { it.updateCache(archetype) } - archetype.isIterating = true return true } @@ -112,7 +108,6 @@ class CachedQuery internal constructor(val query: T) { if (prepareRow()) { query } else { - archetype.isIterating = false n++ if (prepareArchetype()) { prepareRow() @@ -122,19 +117,18 @@ class CachedQuery internal constructor(val query: T) { }.constrainOnce()) } finally { //TODO issues if it's just root archetype? - archetype.isIterating = false closed = true } return collected } - inline fun map(crossinline run: T.(T) -> R): List { + inline fun map(crossinline run: (T) -> R): List { val deferred = mutableListOf() forEach { deferred.add(run(it)) } return deferred } - inline fun mapNotNull(crossinline run: T.(T) -> R?): List { + inline fun mapNotNull(crossinline run: (T) -> R?): List { val deferred = mutableListOf() forEach { query -> run(query).let { if (it != null) deferred.add(it) } } return deferred @@ -143,7 +137,7 @@ class CachedQuery internal constructor(val query: T) { @PublishedApi internal class FoundValue : Throwable() - inline fun any(crossinline predicate: T.(T) -> Boolean): Boolean { + inline fun any(crossinline predicate: (T) -> Boolean): Boolean { try { forEach { if (predicate(it)) throw FoundValue() } } catch (e: FoundValue) { @@ -153,7 +147,7 @@ class CachedQuery internal constructor(val query: T) { return false } - inline fun find(crossinline map: T.(T) -> R, crossinline predicate: T.(T) -> Boolean): R? { + inline fun find(crossinline map: (T) -> R, crossinline predicate: (T) -> Boolean): R? { var found: R? = null try { forEach { @@ -170,7 +164,7 @@ class CachedQuery internal constructor(val query: T) { } @OptIn(UnsafeAccessors::class) - inline fun filter(crossinline predicate: T.(T) -> Boolean): EntityArray { + inline fun filter(crossinline predicate: (T) -> Boolean): EntityArray { val deferred = mutableLongListOf() forEach { if (predicate(it)) deferred.add(it.unsafeEntity.toLong()) } return deferred.toEntityArray(query.world) @@ -179,24 +173,24 @@ class CachedQuery internal constructor(val query: T) { data class Deferred( val data: R, - val entity: GearyEntity + val entity: GearyEntity, ) @OptIn(UnsafeAccessors::class) - inline fun mapWithEntity(crossinline run: T.(T) -> R): List> { + inline fun mapWithEntity(crossinline run: (T) -> R): List> { val deferred = mutableListOf>() forEach { // TODO use EntityList instead - deferred.add(Deferred(run(it), Entity(it.unsafeEntity, world))) + deferred.add(Deferred(run(it), Entity(it.unsafeEntity, it.world))) } return deferred } @OptIn(UnsafeAccessors::class) - fun entities(): List { - val entities = mutableListOf() - forEach { entities.add(Entity(it.unsafeEntity, world)) } - return entities + fun entities(): EntityArray { + val entities = mutableLongListOf() + forEach { entities.add(it.unsafeEntity.toLong()) } + return entities.toEntityArray(query.world) } fun count(): Int { diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/query/QueriedEntity.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/query/QueriedEntity.kt index 49066540b..747965c89 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/query/QueriedEntity.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/query/QueriedEntity.kt @@ -12,6 +12,7 @@ import com.mineinabyss.geary.modules.get import com.mineinabyss.geary.systems.accessors.Accessor import com.mineinabyss.geary.systems.accessors.AccessorOperations import com.mineinabyss.geary.systems.accessors.FamilyMatching +import kotlin.jvm.JvmField open class QueriedEntity( final override val world: Geary, @@ -19,6 +20,7 @@ open class QueriedEntity( ) : AccessorOperations(), Geary by world { @PublishedApi @UnsafeAccessors + @JvmField internal var archetype = world.get().rootArchetype internal val extraFamilies: MutableList = mutableListOf() @@ -42,10 +44,9 @@ open class QueriedEntity( @PublishedApi @UnsafeAccessors + @JvmField internal var row = 0 - private var delegate: GearyEntity? = null - @UnsafeAccessors val unsafeEntity: EntityId get() = this.archetype.getEntity(row) diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/query/QueryShorthands.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/query/QueryShorthands.kt index 0a69a7550..80f087cf4 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/query/QueryShorthands.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/query/QueryShorthands.kt @@ -63,6 +63,22 @@ abstract class ShorthandQuery5(world: Geary) : ShorthandQuery(wor abstract operator fun component5(): E } +abstract class ShorthandQuery6(world: Geary) : ShorthandQuery(world) { + val comp1 get() = component1() + val comp2 get() = component2() + val comp3 get() = component3() + val comp4 get() = component4() + val comp5 get() = component5() + val comp6 get() = component6() + + abstract operator fun component1(): A + abstract operator fun component2(): B + abstract operator fun component3(): C + abstract operator fun component4(): D + abstract operator fun component5(): E + abstract operator fun component6(): F +} + fun Geary.query() = object : Query(this) {} @@ -143,7 +159,29 @@ inline fun Geary.query( size5: QueryShorthands.Size5? = null, noinline filterFamily: (MutableFamily.Selector.And.() -> Unit)? = null, ) = object : ShorthandQuery5(this) { - override val involves = entityTypeOf(cId(), cId(), cId(), cId()) + override val involves = entityTypeOf(cId(), cId(), cId(), cId(), cId()) + override fun ensure() { + filterFamily?.let { this { it() } } + } + + private val accessor1 = getPotentiallyNullable() + private val accessor2 = getPotentiallyNullable() + private val accessor3 = getPotentiallyNullable() + private val accessor4 = getPotentiallyNullable() + private val accessor5 = getPotentiallyNullable() + + override fun component1(): A = accessor1.get(this) + override fun component2(): B = accessor2.get(this) + override fun component3(): C = accessor3.get(this) + override fun component4(): D = accessor4.get(this) + override fun component5(): E = accessor5.get(this) +} + +inline fun Geary.query( + size6: QueryShorthands.Size6? = null, + noinline filterFamily: (MutableFamily.Selector.And.() -> Unit)? = null, +) = object : ShorthandQuery6(this) { + override val involves = entityTypeOf(cId(), cId(), cId(), cId(), cId(), cId()) override fun ensure() { filterFamily?.let { this { it() } } } @@ -153,12 +191,14 @@ inline fun Geary.query( private val accessor3 = getPotentiallyNullable() private val accessor4 = getPotentiallyNullable() private val accessor5 = getPotentiallyNullable() + private val accessor6 = getPotentiallyNullable() override fun component1(): A = accessor1.get(this) override fun component2(): B = accessor2.get(this) override fun component3(): C = accessor3.get(this) override fun component4(): D = accessor4.get(this) override fun component5(): E = accessor5.get(this) + override fun component6(): F = accessor6.get(this) } @JvmName("toList1") @@ -177,4 +217,5 @@ object QueryShorthands { sealed class Size3 sealed class Size4 sealed class Size5 + sealed class Size6 } diff --git a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/queries/accessors/AccessorDataModificationTests.kt b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/queries/accessors/AccessorDataModificationTests.kt index c4b1e0d39..1fefc512c 100644 --- a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/queries/accessors/AccessorDataModificationTests.kt +++ b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/queries/accessors/AccessorDataModificationTests.kt @@ -20,10 +20,10 @@ class AccessorDataModificationTests : GearyTest() { } var count = 0 - registerQuery().forEach { - data shouldBe Comp1(1) - data = Comp1(10) - data shouldBe Comp1(10) + registerQuery().forEach { q -> + q.data shouldBe Comp1(1) + q.data = Comp1(10) + q.data shouldBe Comp1(10) count++ } count shouldBe 1 diff --git a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/systems/RelationMatchingSystemTest.kt b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/systems/RelationMatchingSystemTest.kt index 644300f0f..00c5172c5 100644 --- a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/systems/RelationMatchingSystemTest.kt +++ b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/systems/RelationMatchingSystemTest.kt @@ -5,9 +5,8 @@ import com.mineinabyss.geary.helpers.componentId import com.mineinabyss.geary.helpers.contains import com.mineinabyss.geary.helpers.entity import com.mineinabyss.geary.modules.findEntities -import com.mineinabyss.geary.test.GearyTest -import com.mineinabyss.geary.modules.geary import com.mineinabyss.geary.systems.query.Query +import com.mineinabyss.geary.test.GearyTest import io.kotest.inspectors.forAll import io.kotest.matchers.collections.shouldContain import io.kotest.matchers.collections.shouldNotContain @@ -26,9 +25,9 @@ class RelationMatchingSystemTest : GearyTest() { resetEngine() val system = system(object : Query(this) { val persists by getRelationsWithData() - }).exec { + }).exec { q -> ran++ - persists.forAll { it.data.shouldBeInstanceOf() } + q.persists.forAll { it.data.shouldBeInstanceOf() } } val entity = entity { @@ -60,13 +59,13 @@ class RelationMatchingSystemTest : GearyTest() { val system = system(object : Query(this) { val persists by getRelationsWithData() val instanceOf by getRelationsWithData() - }).exec { + }).exec { q -> ran++ - persistsCount += persists.size - instanceOfCount += instanceOf.size - persists.forAll { it.data.shouldBeInstanceOf() } - persists.forAll { it.targetData shouldNotBe null } - instanceOf.forAll { it.data shouldBe null } + persistsCount += q.persists.size + instanceOfCount += q.instanceOf.size + q.persists.forAll { it.data.shouldBeInstanceOf() } + q.persists.forAll { it.targetData shouldNotBe null } + q.instanceOf.forAll { it.data shouldBe null } } entity { @@ -111,9 +110,9 @@ class RelationMatchingSystemTest : GearyTest() { val system = system(object : Query(this) { val withData by getRelationsWithData() - }).exec { - withData.forAll { it.data shouldBe Persists() } - withData.forAll { it.targetData shouldBe "Test" } + }).exec { q -> + q.withData.forAll { it.data shouldBe Persists() } + q.withData.forAll { it.targetData shouldBe "Test" } } println(componentId()) println(findEntities {