From dda7227e0b62043b1a73712994509accae848fb2 Mon Sep 17 00:00:00 2001 From: "Alexander.Bondarev" Date: Wed, 7 Aug 2024 17:42:37 +0200 Subject: [PATCH] Added "ViewableList.sync" method for optimized updates with a minimal number of change events. --- .../rd/util/reactive/ViewableList.kt | 192 +++++++++++++++++- .../rd/util/test/cases/ViewableListTest.kt | 68 ++++++- 2 files changed, 253 insertions(+), 7 deletions(-) diff --git a/rd-kt/rd-core/src/main/kotlin/com/jetbrains/rd/util/reactive/ViewableList.kt b/rd-kt/rd-core/src/main/kotlin/com/jetbrains/rd/util/reactive/ViewableList.kt index d34a76194..cde3759c7 100644 --- a/rd-kt/rd-core/src/main/kotlin/com/jetbrains/rd/util/reactive/ViewableList.kt +++ b/rd-kt/rd-core/src/main/kotlin/com/jetbrains/rd/util/reactive/ViewableList.kt @@ -3,6 +3,7 @@ package com.jetbrains.rd.util.reactive import com.jetbrains.rd.util.catch import com.jetbrains.rd.util.lifetime.Lifetime import com.jetbrains.rd.util.lifetime.isAlive +import java.util.Objects class ViewableList(private val storage: MutableList = mutableListOf()) : IMutableViewableList { override val change = Signal>() @@ -56,9 +57,9 @@ class ViewableList(private val storage: MutableList = mutableListOf( return changes.isNotEmpty() } - override fun addAll(elements: Collection): Boolean { + private fun addAll(iterator: Iterator): Boolean { val changes = arrayListOf>() - for (element in elements) { + for (element in iterator) { storage.add(element) changes.add(IViewableList.Event.Add(size - 1, element)) } @@ -66,6 +67,8 @@ class ViewableList(private val storage: MutableList = mutableListOf( return changes.isNotEmpty() } + override fun addAll(elements: Collection) = addAll(elements.iterator()) + override fun clear() { val changes = arrayListOf>() for (i in (storage.size-1) downTo 0) { @@ -79,6 +82,24 @@ class ViewableList(private val storage: MutableList = mutableListOf( return filterElementsInplace(elements) { index, elementsSet -> storage[index] in elementsSet } } + fun removeRange(fromIndex: Int, toIndex: Int) { + when (toIndex - fromIndex) { + 0 -> Unit + 1 -> removeAt(fromIndex) + else -> removeRangeSlow(fromIndex, toIndex) + } + } + + private fun removeRangeSlow(fromIndex: Int, toIndex: Int) { + val changes = buildList>(toIndex - fromIndex) { + for (i in (toIndex - 1) downTo fromIndex) { + add(IViewableList.Event.Remove(i, storage[i])) + } + } + storage.subList(fromIndex, toIndex).clear() + changes.forEach { change.fire(it) } + } + private inline fun filterElementsInplace(elements: Collection, predicate: (Int, Set) -> Boolean): Boolean { val elementsSet = elements.toSet() val changes = arrayListOf>() @@ -107,7 +128,172 @@ class ViewableList(private val storage: MutableList = mutableListOf( override fun listIterator(): MutableListIterator = MyIterator(storage.listIterator()) override fun listIterator(index: Int): MutableListIterator = MyIterator(storage.listIterator(index)) - override fun subList(fromIndex: Int, toIndex: Int): MutableList = throw UnsupportedOperationException() + override fun subList(fromIndex: Int, toIndex: Int): MutableList { + Objects.checkFromToIndex(fromIndex, toIndex, size) + return MySubList(fromIndex, toIndex - fromIndex) + } + + /** + * Synchronizes the viewable list by adding missing elements and removing unmatched elements. + * If the order of equal values is not changed, then they won't be modified. + * However, even if equal elements exist in both lists, + * but order is swapped, then they will be removed and re-added to satisfy the new values order. + * It helps drastically reduce the number of change events if the collection is unmodified at all + * or just a few elements are changed compared to the classical approach with 'clear' and 'addAll'. + * + * @param newValues the new values to be synced with + */ + fun sync(newValues: Collection): Boolean { + if (isEmpty()) { + return addAll(newValues) + } + + if (newValues.isEmpty()) { + clear() + return true + } + + val iterator = iterator() + val newIterator = newValues.iterator() + + var index = 0 + var newValue: T + while (true) { + newValue = newIterator.next() + if (newValue != iterator.next()) + { + replaceTailSlow(index, newValue, newIterator) + return true + } + ++index + if (!newIterator.hasNext()) { + removeRange(index, size) + return true + } + if (!iterator.hasNext()) { + return addAll(newIterator) + } + } + } + + private fun replaceTailSlow(firstUnmatchedIndex: Int, firstUnmatchedValue: T, newIterator: Iterator) { + fun matchIndex(items: MutableMap, value: T, fromIndex: Int): Int? { + val matchedIndex = items.remove(value) + if (matchedIndex is Int) { + return if (matchedIndex >= fromIndex) matchedIndex else null + } + @Suppress("UNCHECKED_CAST") + (matchedIndex as? ArrayDeque)?.let { + while (matchedIndex.size > 0) { + val endIndex = matchedIndex.removeFirst() + if (endIndex >= fromIndex) { + if (matchedIndex.size > 0) { + items[value] = matchedIndex + } + return endIndex + } + } + } + return null + } + + val items = mutableMapOf() + var newValue = firstUnmatchedValue + for (index in firstUnmatchedIndex until size) { + val item = this[index] + val itemIndex = items[item] + @Suppress("UNCHECKED_CAST") + when (itemIndex) { + is Int -> items[item] = ArrayDeque().apply { + add(itemIndex) + add(index) + } + is ArrayDeque<*> -> (itemIndex as ArrayDeque).add(index) + null -> items[item] = index + } + } + + val changes = ArrayDeque>() + val originalSize = size + var insertIndex = firstUnmatchedIndex + var processedIndex = firstUnmatchedIndex + var matchedIndex: Any? + while (true) { + matchedIndex = matchIndex(items, newValue, processedIndex) + if (matchedIndex != null) { + val removeCount = matchedIndex - processedIndex + if (removeCount > 0) { + for (removeIndex in processedIndex until matchedIndex) { + changes.addFirst(IViewableList.Event.Remove(removeIndex, storage[removeIndex])) + } + } + processedIndex = matchedIndex + 1 + storage.add(storage[matchedIndex]) + ++insertIndex + } + else { + changes.add(IViewableList.Event.Add(insertIndex++, newValue)) + storage.add(newValue) + } + if (!newIterator.hasNext()) + break + newValue = newIterator.next() + } + + // If last new value was matched then we generate remove events after all "add" + // events so last "remove" event will match the tail for "viewTail" extension property. + // Otherwise, we keep an "add" event for the last element and generate all "remove" events at the beginning + if (matchedIndex != null) { + val addedElementsAdjustment = insertIndex - processedIndex + for (removeIndex in originalSize - 1 downTo processedIndex) { + changes.add(IViewableList.Event.Remove(removeIndex + addedElementsAdjustment, storage[removeIndex])) + } + } + else { + for (removeIndex in processedIndex until originalSize) { + changes.addFirst(IViewableList.Event.Remove(removeIndex, storage[removeIndex])) + } + } + + storage.subList(firstUnmatchedIndex, originalSize).clear() + + changes.forEach { change.fire(it) } + } + + private inner class MySubList(private val fromIndex: Int, size: Int) : AbstractMutableList() { + var mySize = size + + override val size get() = mySize + + override fun add(index: Int, element: T) { + Objects.checkIndex(index, mySize + 1) + this@ViewableList.add(fromIndex + index, element).also { ++mySize } + } + + override fun get(index: Int): T { + Objects.checkIndex(index, mySize) + return this@ViewableList[index] + } + + override fun removeAt(index: Int): T { + Objects.checkIndex(index, mySize) + return this@ViewableList.removeAt(index).also { --mySize } + } + + override fun set(index: Int, element: T): T { + Objects.checkIndex(index, mySize) + return this@ViewableList.set(index, element) + } + + override fun subList(fromIndex: Int, toIndex: Int): MutableList { + Objects.checkFromToIndex(fromIndex, toIndex, mySize) + return MySubList(this.fromIndex + fromIndex, toIndex - fromIndex) + } + + override fun clear() { + this@ViewableList.removeRange(fromIndex, fromIndex + size).also { mySize = 0 } + } + } private inner class MyIterator(val baseIterator: MutableListIterator): MutableListIterator by baseIterator { override fun add(element: T) { diff --git a/rd-kt/rd-core/src/test/kotlin/com/jetbrains/rd/util/test/cases/ViewableListTest.kt b/rd-kt/rd-core/src/test/kotlin/com/jetbrains/rd/util/test/cases/ViewableListTest.kt index 89c3184c4..62569ddb1 100644 --- a/rd-kt/rd-core/src/test/kotlin/com/jetbrains/rd/util/test/cases/ViewableListTest.kt +++ b/rd-kt/rd-core/src/test/kotlin/com/jetbrains/rd/util/test/cases/ViewableListTest.kt @@ -3,11 +3,11 @@ package com.jetbrains.rd.util.test.cases import com.jetbrains.rd.util.lifetime.Lifetime import com.jetbrains.rd.util.lifetime.plusAssign import com.jetbrains.rd.util.reactive.IMutableViewableList +import com.jetbrains.rd.util.reactive.IViewableList import com.jetbrains.rd.util.reactive.ViewableList +import com.jetbrains.rd.util.reactive.viewableTail import com.jetbrains.rd.util.test.framework.RdTestBase -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue +import kotlin.test.* class ViewableListTest : RdTestBase() { @Test @@ -43,7 +43,7 @@ class ViewableListTest : RdTestBase() { Lifetime.using { lifetime -> list.view(lifetime) { lt, value -> log.add("View $value"); lt += { log.add("UnView $value") } } list.add(0) - list.set(0, 1); + list[0] = 1 list.remove(0) } @@ -151,4 +151,64 @@ class ViewableListTest : RdTestBase() { assertTrue(list.add(0)) } } + + @Test + fun testSync() { + val items = ViewableList(mutableListOf(1, 2, 3)) + items.assertSync(listOf(1, 2, 3), emptyList(), emptyList()) + items.assertSync(listOf(3, 2, 1), listOf(2 to 1, 1 to 2), listOf(2 to 1, 1 to 0)) + items.assertSync(listOf(4, 3, 2, 1, 0), listOf(4 to 0, 0 to 4), emptyList()) + items.assertSync(listOf(3, 2, 1), emptyList(), listOf(4 to 0, 0 to 3)) + items.assertSync(listOf(4, 2, 0), listOf(4 to 0, 0 to 2), listOf(1 to 2, 3 to 0)) + items.assertSync(emptyList(), emptyList(), listOf(0 to 2, 2 to 1, 4 to 0)) + items.assertSync(listOf(1, 2, 3, 4, 5), listOf(1 to 0, 2 to 1, 3 to 2, 4 to 3, 5 to 4), emptyList()) + items.assertSync(listOf(2, 1, 3, 5, 4), listOf(1 to 1, 4 to 4), listOf(4 to 3, 1 to 0)) + items.assertSync(listOf(2, 1, 4), emptyList(), listOf(5 to 3, 3 to 2)) + items.assertSync(listOf(2, 3, 1, 5, 4), listOf(3 to 1, 5 to 3), emptyList()) + items.assertSync(listOf(2, 2, 3, 3, 1, 1, 5, 5, 4, 4), listOf(2 to 1, 3 to 3, 1 to 5, 5 to 7, 4 to 9), emptyList()) + items.assertSync(listOf(2, 2, 3, 1, 1, 5, 4, 4), emptyList(), listOf(5 to 7, 3 to 3)) + items.assertSync(listOf(2, 2, 2, 5, 5, 5), listOf(2 to 2, 5 to 4, 5 to 5), listOf(4 to 7, 4 to 6, 1 to 4, 1 to 3, 3 to 2)) + items.assertSync(listOf(2, 5), emptyList(), listOf(2 to 2, 2 to 1, 5 to 3, 5 to 2)) + } + + @Test + fun testViewableTail() { + val items = ViewableList(mutableListOf(1, 2, 3)) + Lifetime.using { lifetime -> + val tail = mutableListOf() + items.viewableTail().advise(lifetime) { tail.add(it) } + items.add(4) + items.addAll(listOf(5, 6, 7)) + items.remove(6) + items.remove(7) + items.removeAll(listOf(2, 3, 4, 5, 6, 7)) + items.sync(listOf(2, 3)) + items.sync(listOf(1, 2)) + assertContentEquals(listOf(3, 4, 7, 5, 1, 3, 2), tail) + } + } + + private fun ViewableList.assertSync(expectedItems: List, expectedAdded: List>, expectedRemoved: List>) { + assertItemsAndChanges(expectedItems, expectedAdded, expectedRemoved) { + sync(expectedItems) + } + } + + private fun ViewableList.assertItemsAndChanges(expectedItems: List, expectedAdded: List>, expectedRemoved: List>, action: ViewableList.() -> Unit) { + Lifetime.using { lifetime -> + val added = mutableListOf>() + val removed = mutableListOf>() + change.advise(lifetime) { + when (it) { + is IViewableList.Event.Add -> added.add(it.newValue to it.index) + is IViewableList.Event.Remove -> removed.add(it.oldValue to it.index) + is IViewableList.Event.Update -> {} + } + } + action() + assertContentEquals(expectedItems, this) + assertContentEquals(expectedAdded, added) + assertContentEquals(expectedRemoved, removed) + } + } } \ No newline at end of file